JoyJoy Docs

Tutorial

Learn terminal-native product management step by step.

Learn terminal-native product management step by step.

This tutorial walks you through a complete Joy project setup using a practical example: building a recipe app called Cookbox. By the end, you will know how to create items, track progress, manage dependencies, set milestones, and work with your team.

Contents

TL;DR

mkdir cookbox && cd cookbox && git init
joy init
joy add epic "Recipe Management"
joy add story "Add a recipe" --parent CB-0001 --priority high
joy add task "Set up database" --parent CB-0001 --priority critical
joy start CB-0003
joy deps CB-0002 --add CB-0003
joy milestone add "MVP" --date 2026-04-01
joy milestone link CB-0002 CB-MS-01
joy submit CB-0003
joy close CB-0003
joy

That's the whole loop. Read on for the details.


Setting Up a Project

Create a fresh project:

mkdir cookbox && cd cookbox
git init
joy init

Joy creates a .joy/ directory inside your repo:

.joy/
  project.yaml           Project name, acronym, members, settings
  config.defaults.yaml   Project defaults (committed)
  config.yaml            Personal overrides (gitignored)
  items/                 All your items live here (YAML files)
  milestones/            Milestone definitions
  logs/                  Event log (audit trail)

Everything is plain text, versioned with git. No database, no cloud dependency. If your hard drive survives, your project plan survives.

You can also name your project explicitly:

joy init --name "Cookbox" --acronym CB

Joy also installs a commit-msg hook that enforces item references in every commit message. This is part of the audit trail - every code change must link to a Joy item. More on this in Audit Trail and Releases.

Joining an Existing Project

If you clone a repo that already uses Joy, run the same command:

git clone https://github.com/example/cookbox.git
cd cookbox
joy init

Joy detects the existing project and switches to onboarding mode: it installs the commit-msg hook and sets up your local environment without touching project data.

After onboarding, set up AI tool integration if you use one:

joy ai init

Creating Items

Start with an epic - the big picture:

joy add epic "Recipe Management"

Joy assigns ID CB-0001 and creates .joy/items/CB-0001-recipe-management.yaml.

Now break it down into smaller pieces:

joy add story "Add a recipe" --parent CB-0001 --priority high
joy add story "Edit a recipe" --parent CB-0001 --priority high
joy add story "List recipes with filters" --parent CB-0001
joy add task "Set up SQLite database" --parent CB-0001 --priority critical --effort 3

Effort

Estimate work with --effort on a 1-7 scale: 1=trivial, 2=small, 3=medium, 4=large, 5=major, 6=heavy, 7=massive. It's optional but helps with planning.

Item Types

TypeWhen to use
epicLarge initiative grouping multiple items
storyUser-facing functionality ("As a user, I can...")
taskTechnical work, not directly visible to users
bugSomething is broken
reworkRefactoring or improvement of existing code
decisionArchitecture or product decision to document
ideaNot yet refined - just capture it before it escapes
jobAssignment of work over a scope of items, for an assignee to execute

All items start with status new. Priorities: extreme, critical, high, medium (default), low.

job is the one type with a third positional - the comma-separated scope of items the job covers:

joy add job "Implement recipe management" CB-0002,CB-0005

Jobs live in .joy/jobs/ in their own number space and never appear in default views (joy ls -J lists them). See Jobs on the Workflow page for the full lifecycle.


Filtering and Searching

List all items:

joy ls

Filter to find exactly what you need:

joy ls --type story              # Only stories
joy ls --priority critical       # Only critical items
joy ls --parent CB-0001          # Children of an epic
joy ls --status open             # Only open items
joy ls --members alice@team.com  # Assigned to a specific member
joy ls --members me              # Assigned to you (or --mine)
joy ls --members none            # No assignees
joy ls --members '*'             # Has at least one assignee
joy ls --milestone CB-MS-01      # In a specific milestone
joy ls --blocked                 # Items with unfinished dependencies
joy ls --tag ui                  # Items tagged with "ui"

Search by text across all items:

joy find "database"              # Search titles and descriptions

Tags

Tags are free-text labels for cross-cutting categories - things like ui, backend, security, or tech-debt:

joy add task "Fix layout" --tags "ui,urgent"
joy edit CB-0004 --tags "ui,search"

Tags are comma-separated. Using --tags replaces all existing tags. Use --tags "" to clear them.

Views

joy                              # Board view (items grouped by status)
joy ls --tree                    # Hierarchy view (parent/child tree)
joy show CB-0002                 # Full detail view with comments

Managing Dependencies

Dependencies let you express ordering between items. For example, you need the database before you can add recipes.

joy deps CB-0002 --add CB-0005

This means: CB-0002 (Add a recipe) depends on CB-0005 (Set up SQLite database). CB-0005 must be completed first.

joy deps CB-0002                 # List dependencies
joy deps CB-0002 --tree          # Show full dependency tree
joy deps CB-0002 --rm CB-0005   # Remove a dependency

Joy detects circular dependencies and refuses to create them.


Tracking Progress

The status workflow:

new -> open -> in-progress -> review -> closed
         \                      |
          +---> deferred <------+

Move items through the pipeline:

joy status CB-0005 open          # Approve for work
joy start CB-0005                # Shortcut: set to in-progress
joy stop CB-0005                 # Shortcut: back to open
joy submit CB-0005               # Shortcut: set to review
joy close CB-0005                # Shortcut: set to closed
joy reopen CB-0005               # Reopen a closed/deferred item

If an item depends on something unfinished, Joy warns you but does not block. When all children of an epic are closed, the epic auto-closes.

Assignments and Comments

joy assign CB-0005               # Assign to yourself (git email)
joy assign CB-0005 pete@phoenix.org  # Assign to someone else
joy comment CB-0005 "Schema looks good, all migrations pass."
joy comment CB-0005              # Opens $EDITOR for a longer note
joy comment edit CB-0005 1 "Schema looks good (verified all migrations)."
joy comment rm CB-0005 2 --force # Delete comment #2

joy comment <ID> without TEXT opens your editor on an empty tempfile; saving an empty buffer aborts. Editor resolution: --editor <cmd>, then joy config set editor, then $VISUAL, then $EDITOR. Comment indices for edit and rm are 1-based and match what joy show <ID> prints.

When starting an item (joy start), Joy auto-assigns it to you if no one is assigned yet.


Working with Milestones

Milestones mark deadlines and group related work.

joy milestone add "MVP" --date 2026-04-01

Link items to the milestone:

joy milestone link CB-0002 CB-MS-01
joy milestone link CB-0003 CB-MS-01
joy milestone link CB-0005 CB-MS-01

Check progress:

joy milestone show CB-MS-01      # Progress, risks, blocked items
joy milestone ls                 # All milestones with counts
joy roadmap                      # Full roadmap tree view

Children inherit their parent's milestone automatically. If CB-0001 is linked to CB-MS-01, all its children are too - unless they override it.


Audit Trail and Releases

Joy keeps a structured event log that records every action automatically.

joy log                          # Last 20 events
joy log --since 7d               # Last 7 days
joy log CB-0005                  # Events for a specific item
joy log --limit 50               # Show more entries

Every joy command leaves a trace in .joy/logs/ - one file per day, append-only, timestamped to the millisecond:

2026-03-11T16:14:32.320Z CB-0005 item.created [mac@phoenix.org]
2026-03-11T16:15:01.440Z CB-0005 item.status_changed "new -> in-progress" [mac@phoenix.org]
2026-03-11T16:42:18.100Z CB-0005 comment.added [pete@phoenix.org]
2026-03-11T17:00:00.000Z CB-0005 comment.added [claude delegated-by:mac@phoenix.org]

The log records only structural facts: who did what, when, on which item. Titles, descriptions, and comment text are not written to the log - they live in the item file itself, behind whatever Crypt zone protects it. The log stays as a faithful audit trail even when item content is later encrypted. State transitions (new -> in-progress), member IDs, and item / milestone IDs do appear, because they are needed to interpret the event.

These logs are committed to git with your project. Every team member's actions are recorded - a built-in audit trail. When an AI tool acts on behalf of a human, the log shows both identities via delegated-by.

Commit-Msg Hook

Joy installs a commit-msg hook (via joy init) that enforces every commit message references at least one item ID:

git commit -m "feat(db): add migration CB-0005"     # OK
git commit -m "fix typo"                             # REJECTED

The hook reads the project acronym from .joy/project.yaml and checks for the pattern CB-XXXX. For commits that genuinely have no item (CI config, dependency bumps), use the [no-item] tag:

git commit -m "chore: bump dependencies [no-item]"  # OK

In multi-repo setups (umbrella with submodules), each subproject has its own acronym.

Releases

A release in Joy is three explicit steps. Joy never reaches into your build system; it just updates version strings, writes a release record, and talks to your forge. Anything ecosystem-specific (lockfile refresh, uploading to a package registry, running tests) happens between the Joy steps in your project's own release script.

joy release bump patch               # Step 1: replace "X.Y.Z" in configured files
# ... project-specific steps go here (e.g. refresh a lockfile) ...
joy release record patch             # Step 2: record + commit + tag (local only)
# ... project-specific steps go here (e.g. upload to a registry) ...
joy release publish                  # Step 3: push + forge release

joy release bump replaces every quoted occurrence of the current version with the next one across the files listed under release.version-files in project.yaml. Plain text substitution, no TOML/JSON/YAML parsing, so it catches any workspace dependency pins that happen to reference the same version.

joy release record collects items closed since the last release, writes the snapshot to .joy/releases/, commits the bumped files, and tags locally. Nothing has been pushed, so a typo rolls back with git reset --hard HEAD~1 && git tag -d vX.Y.Z.

joy release publish pushes commits and tag, then creates the forge release. The forge is auto-detected from your git remotes - a single supported remote is used silently, multiple supported remotes prompt on a TTY (or require --forge in CI). Today only GitHub has a release backend; on the other forges the tag is pushed and the release step is skipped with a warning. The release is created through the same connector and the same credential as the rest of joy, so the sign in below is what signs it in.

Override the auto-detection when you need to:

joy project set forge github         # lock in a specific forge
joy project set forge none           # explicit opt-out: push the tag only
joy project set forge ""             # clear the override, return to auto-detect
joy release publish --forge none     # one-shot opt-out for this run

Preview and browse without touching anything:

joy release show                     # Preview from event log
joy release show v1.0.0              # Show an existing release
joy release ls                       # List all releases

Editing and Deleting

joy edit CB-0002 --priority critical
joy edit CB-0002 --title "Add and validate a recipe"
joy edit CB-0002 --type bug          # Change item type
joy rm CB-0006                       # Delete (asks for confirmation)
joy rm CB-0001 -rf                   # Delete epic and all children

Signing in to a Forge

Not in a joy release yet

joy forge arrives with the release that ships the joy-forge connector. The newest release is v0.20.0 and it carries neither. Joy's own OAuth applications for github.com, gitlab.com and codeberg.org are not registered yet either, so on those three hosts joy forge login answers with that fact and names the two ways that do work: joy forge login --token-stdin, or a client_id for the host in forges.yaml.

A local project needs no forge at all. Once it has a remote on one, Joy talks to it for chats, which fetch and push on every read and every message, and for joy release publish, which pushes the tag and creates the release. A project that sets workflow.auto-git to push adds a third: the push after every write.

Most of the time there is nothing to set up. Joy carries its own Git engine and reads the setup you already have: your ssh agent and the key files your ssh config names, your Git credential helper for an https remote, and the login of gh, glab or tea where one of them is signed in to the host. None of that needs a Joy command.

When none of it answers, sign in from Joy itself:

joy forge login                      # The host of this project's remote
joy forge login --host github.com    # Or a host you name
joy forge status                     # One row per host: login, state, source
joy forge logout --host github.com   # Remove what this machine holds

joy forge login runs the forge's own sign in flow and prints the address to open and the code to enter while it waits. It opens no browser for you, so the browser may be on another machine. --for read|write|create|release asks for the rights that level needs, and --login <name> signs in as one of several logins on a host.

On a machine with nobody sitting at it, hand a token over instead:

joy forge login --host codeberg.org --token-stdin < token.txt

That reads exactly one line from standard input and checks it with the forge before storing it. A token is never a command line argument, so no process list can carry it, and there is no --token <value>.

joy forge logout removes what Joy stored. Where the credential came from gh, glab or tea, Joy removes nothing and prints that tool's own command instead.

A self-hosted instance is claimed on any of three grounds: gh, glab or tea is already signed in to that host, an operator named the instance in forges.yaml, or the project sets its forge: itself. The file is the way out of the circle where nobody can sign in through Joy because Joy does not claim the host.


AI Tool Integration

Joy integrates with AI coding tools so they can manage your backlog alongside you.

joy ai init

This does four things:

  1. Checks if your project has the Vision, Architecture, and Contributing docs (offers to create templates if missing).
  2. Bootstraps your authentication inline if joy auth init has not run yet, so the whole setup is one passphrase.
  3. Detects your installed AI tools (Claude Code, Qwen Code, Mistral Vibe, Google Antigravity's agy, GitHub Copilot) and writes their instruction files plus the /joy skill where the tool supports skills.
  4. Registers each detected tool as an AI member under the tool's name (claude, vibe), with what the project lets a new AI member do.

The generated instructions are intentionally short: they point the AI at joy ai tutorial as the operational guide and tell it to use the member, session_env, and delegated_by fields returned by its token redemption. No tool-specific identity and no co-author attribution is required: Joy writes Delegated-By: for an AI commit and names no tool.

GitHub Copilot needs no special handling, in the editor or out of it. joy ai init finds it under copilot, under gh copilot, and in a VS Code-family editor even when neither command is installed: Copilot Chat is built into VS Code and reads the very files joy writes. All of it is the one member copilot. Where the editor cannot be seen from (inside tmux, over ssh, under sudo), name it directly with joy ai init --tool copilot.

Google Antigravity is found under agy. If agy is not on your shell's PATH (for example, you only use the Antigravity editor), configure it explicitly with joy ai init --tool agy. It registers agy. Antigravity and Mistral Vibe share the root AGENTS.md, which contains no tool identity; resetting one of the two keeps that file while the other still uses it.

For an AI joy genuinely cannot detect, such as a chat assistant with no CLI and no instruction files of its own, register a member by hand:

joy project member add my-assistant

For an AI member joy project member add skips the OTP machinery and prints the next steps for issuing a delegation token.

The Trust Model

Joy's AI Governance is built on five pillars: Trustship (who do I trust?), Guardianship (what do I protect against?), Orchestration (how do I steer work?), Traceability (what happened?), and Settlement (what did it cost?).

Together they form the Trust Model - the configuration that governs how humans and AI members collaborate. It scales naturally: a solo developer has implicit trust (one member, all capabilities, no gates). A team adds explicit trust (members with specific capabilities). An enterprise adds verified trust (gates, cost limits, audit trails). Same workflow, growing accountability.

The rest of this section covers the parts you can use today: identity and capabilities (Trustship), gates (Guardianship), and the event log (Traceability). Job budgets and costs (Settlement) are covered on the Workflow page; routing work to actors automatically (Orchestration) is planned.

AI Identity

AI tools are registered as project members, each known by its name:

joy project member add claude          # detected automatically by `joy ai init`
joy project member add my-assistant   # manual entry for an AI joy cannot detect

When an AI runs a Joy command, it authenticates with the delegation token you handed it; the token tells the CLI which AI member is acting and which human delegated. The event log traces accountability back to that human:

[claude delegated-by:mac@phoenix.org]

AI members have the same capabilities as human members, with one exception: an AI member never holds manage (adding members, changing what the project allows, modifying project settings). Management stays with humans.

Keeping Instructions Current

You usually do not have to run anything explicitly. Every joy invocation checks whether this clone is in sync with the running binary and quietly refreshes the AI instruction files (and the rest of the joy-managed state) when it sees a version mismatch. When that happens joy prints a one-line joy X.Y.Z: synced this repo (...) notice on stderr; if your AI tool's instruction file is mentioned, re-read it before continuing.

For an explicit audit:

joy update --check               # Read-only: every joy-managed artefact
joy update                       # Refresh anything that is stale

See "Updating joy" below for the full picture.


Project Configuration

Joy starts with zero ceremony. No gates, no approvals, no bureaucracy. Add rules only when you need them.

Project Metadata

joy project                      # View project metadata and members
joy project get language          # Get a specific value
joy project set name "Cookbox Pro"   # Set a value (requires manage)
joy project set language de       # Change project language

Settable keys: name, description, language, forge, privacy, and docs.*. Read-only: acronym, created.

Members and Capabilities

Joy tracks project members and their capabilities. The founding member is added during joy init: from joy init --user <address>, else from git config user.email, and on a terminal joy asks for the address when neither is set. Further members are added manually:

joy project member add pete@phoenix.org
joy project member add claude --capabilities implement review
joy project member show pete@phoenix.org
joy project member rm pete@phoenix.org

Each member is a file of its own under .joy/members/; project.yaml lists them.

Joy defines twelve capabilities across two groups.

Lifecycle capabilities govern what a member can do on items:

CapabilityWhat it grants
conceiveFrame a problem and propose direction (typically on idea/epic).
planBreak work down: scope, effort, milestones.
designSettle the technical approach for an item.
implementWrite the code or content.
testVerify behaviour and add tests.
reviewApprove work from someone else and gate submit -> closed.
documentUpdate user- or developer-facing docs.
jobsTake jobs, and move them through their statuses: approve a job so its assignee takes it, stop it, accept the result.

Management capabilities govern project-level operations:

CapabilityWhat it grants
createCreate new items (joy add).
assignAssign items to members (joy assign).
manageAdd/edit members, change project settings.
deleteRemove items (joy rm).

joy project member add defaults to the seven work capabilities from conceive to document plus create and assign, for a person and for an AI member alike. jobs, manage and delete must be granted explicitly, and an AI member never holds manage.

Change what a member may do with joy project member edit:

joy project member edit pete@phoenix.org --add-capability review
joy project member edit pete@phoenix.org --capabilities plan implement test

What an AI Member May Do

A person has capabilities. An AI member has two settings: its capabilities and one interaction level, which says how much it does on its own:

  • proposing - proposes and waits; it changes nothing by itself
  • confirmed - works and asks before each step that changes something
  • autonomous - works on its own and reports the result

A new AI member starts autonomous. The level reaches the tool: joy puts the AI tool into the matching mode when the tool is set up and on every chat turn, and a job runs at the level it was released at.

Two people have a say, and both sign what they say with their own key:

  1. The project says what the AI member may do at most. A member with manage sets it:

    joy project member edit claude --project --capabilities implement review --level confirmed
    joy project member edit claude --project --model opus
  2. You say what it may do when it works for you, within what the project allows. Nobody needs manage for that:

    joy project member edit claude --level proposing
    joy project member edit claude --rm-capability review

The model works the same way round, without a signature, because a model is no permission. A member with manage sets it for everybody with --project --model <model>, and --project --model "" leaves the choice to each person. While the project names none, you pick your own:

joy project member edit claude --model sonnet   # for you
joy project member edit claude --model ""       # back to what the tool takes by itself

What comes out for you is the narrower of the two. Inspect it with:

joy project member show claude
               project      mine         effective
  implement    x            x            x
  review       x            -            -
  level        confirmed    proposing    proposing

project is what the project allows, mine what you allow for yourself (empty while you follow the project), effective what the AI member may do when it works for you. Tools and AI agents follow the effective column; they do not re-derive it.

A job runs at one of two levels: proposing, which is what every job has until you say otherwise, or autonomous (joy add job ... --level autonomous), and autonomous only where its assignee may run at autonomous for you. The level in between asks a person before a command runs, and a job has nobody there to ask. From the approval on the level is the job's own: a member changed later does not reach into the job. You can still switch it yourself as long as the job is not finished (joy edit <ID> --level autonomous). That is how a job that proposed goes on to do the work: at proposing the assignee first settles with you in the job's comments how the items are to be done, then writes its proposal into each scope item as a comment, and reports back in the job. Switch the job to autonomous once you agree.

A project from before this layout keeps working as it is. The first person who signs in with their passphrase brings it over to member files, once and without a question.

Gates (Status Rules)

By default every status transition is allowed. Add gates only when the project needs them. Gates live in .joy/project.yaml under status_rules:

status_rules:
  "review -> closed":
    allow_ai: false        # AI members may not close items
  "in-progress -> review":
    allow_ai: true

Today only allow_ai is honored at runtime; more rule kinds (e.g. requires_role, requires_ci) are part of the vision and not yet enforced. The key is "<from> -> <to>" with the status names as joy status spells them. Job gates use the same form with a job: prefix.

Anonymous Privacy Mode

By default a project is open: each member entry in .joy/project.yaml carries the member's e-mail in cleartext. A project can instead run anonymous, where no e-mail or name is written to the versioned files.

joy init --anonymous                 # start a new project anonymous (asks for a passphrase)
joy project set privacy anonymous    # or switch an existing project (needs auth + manage)
joy project set privacy open         # switch back

In anonymous mode each member is keyed by an opaque id, and the cleartext e-mail lives only in .joy/members.yaml, encrypted per member. Joy resolves ids back to e-mails for you while your session is active. The Git committer identity in each commit is out of scope: Joy keeps only its own files free of cleartext personal data. To honour a deletion request (GDPR Art. 17), erase a member's e-mail and name while keeping the opaque id and the audit trail:

joy project member erase someone@example.com

Solo to Enterprise

Joy scales to the level of ceremony you actually need:

  • Solo: one member, capabilities: all, no status_rules. Run joy init and start working.
  • Small team: add members with explicit capability sets (e.g. AI tools restricted to implement,review,document). Interaction levels stay at project defaults.
  • Enterprise: turn on gates (status_rules), set the level each AI member may work at, set allow_ai: false on transitions where humans must sign off, and rely on the event log for audit.

The same workflow works at every scale - you only opt into more controls.

Configuration Layering

Joy uses layered configuration where each layer overrides the one below:

Layer 4: .joy/config.yaml            Your personal project overrides (gitignored)
Layer 3: ~/.config/joy/config.yaml   Your global settings (all projects)
Layer 2: .joy/config.defaults.yaml   Project defaults (committed, shared)
Layer 1: Code defaults               Built-in fallbacks

View the resolved configuration:

joy config                       # Show all resolved values with sources
joy config get workflow.auto-assign  # Get a specific value
joy config set output.emoji true     # Set a personal override

joy config set always writes to your personal .joy/config.yaml - your preferences never affect teammates. Project defaults in config.defaults.yaml set the shared baseline that the whole team inherits.

Key settings:

SettingDefaultWhat it does
workflow.auto-assigntrueAuto-assign items on joy start
output.colorautoColor mode: auto, always, never
output.emojifalseShow emoji indicators in output
output.shorttrueCompact list output (abbreviations)
output.fortunetrueShow occasional quotes in output
auto-synctrueRefresh joy-managed state when the binary version moves ahead of this clone's marker

Updating joy

Joy keeps two things current: the joy binary on your machine, and the joy-managed artefacts in each clone (.gitattributes, the YAML merge driver registration, the CI file that keeps pull requests mergeable, the commit-msg hook, SECURITY.md, AI tool instruction files, ...). One command handles both:

joy update                       # Swap binary + refresh in-repo state
joy update --check               # Read-only audit of every joy-managed artefact
joy update --no-binary           # In-repo refresh only
joy update --json                # Same, machine-readable envelope

The binary swap is receipt-gated: only builds installed through the cargo-dist installer carry the receipt that lets joy update itself in place. Builds installed via cargo install, WinGet, or a distro package skip the swap with a clear message and ask you to use the installer that placed the binary.

Auto-sync: the in-repo half is implicit

You almost never have to run joy update for the in-repo refresh. Every joy invocation cheaply compares the running binary's version against joy.last-sync-version in this clone's local git config and silently catches up when they differ. When that happens you see one stderr line, e.g. joy 0.15.0: synced this repo (previous marker: 0.14.2). If your AI tool's instruction file is mentioned, re-read it. Set auto-sync: false in .joy/config.yaml to opt out per project.

Downgrade guard

If the repo was last synced by a newer joy binary than the one you are running, joy refuses to roll repo state back. The first joy invocation prints a one-line warning, joy update --check reports the version marker as stale, and joy update runs the binary swap (so you can catch up) but skips the in-repo refresh from this still-running OLD process. Open a new shell once the new binary is in $PATH and the auto-sync (or another joy update) does the rest.


Chats

Every project has team chats, and they live where the project lives: in the repository, on refs/joy/chats, sealed for their participants (see AI Tool Integration). The apps and the CLI open the same chats.

joy chat ls                      # Every chat you are in, newest first
joy chat show 10                 # Read a chat (this marks it read, as opening it in the app does)
joy chat show 10 --unread        # Only what you have not read yet
joy chat show 10 --last 5        # Only the last five messages
joy chat show 10 --since 2h      # Only the last two hours (30m, 2h, 3d)
joy chat send 10 "On my way."    # Say something
joy chat info 10                 # Who is in, who has read what

Chat Ids

A chat has an id like an item: MPS-CHAT-0010-BB, the project's acronym, CHAT, a number, and a short suffix. In commands the number is enough (10, 0010), as is MPS-CHAT-0010, the full id, the chat's name, or general for the team-wide chat. joy chat ls prints the full id; the apps show the number in the sidebar and the suffix only when two chats share a number.

Reading and Sending

Reading fetches the chat from the forge first, so show and ls always print the forge's state. Sending appends locally and pushes at once, one forge contact per message; when someone else pushed first, the CLI fetches, unites and pushes again by itself. A chat created in the app is unknown to the CLI until a read fetched it, so run joy chat ls before writing into a chat you have not seen from the terminal yet.

AI Members in Chats

An AI answers a mention (@vibe ... at the start of a message) only in the apps, and only under a delegation from the person addressing it: the app that sends the message starts the turn, on the platform for a platform project and on your machine for a local one, with your delegation, your key and your budget. The CLI has no chat session and no turn host, so joy chat send refuses a message that opens with the mention of an AI member and says why. A mention later in the text only refers to the AI and is sent as it is.

An AI can send from the CLI itself: with its session (--session, or JOY_SESSION) the message is posted as the AI member, marked as delegated by the person whose token the session came from.

Merging Branches

Your items, comments and logs are files in your repository, so they take part in every merge. Two branches that touched the same item are a merge like any other, and joy resolves it: a status from one side and a comment from the other end up in one item, and the day log keeps both sides' lines.

In your clone this is automatic. joy init registers the rules (.gitattributes plus a merge driver in your git config), and every git merge, git pull and git rebase uses them.

Why the button on your forge needs help

GitHub, GitLab and Gitea merge on their own servers with plain git, and they do not run your merge driver: GitHub ignores the repository's .gitattributes altogether, GitLab reads only the built-in rules, and Gitea would need joy installed on the server. A pull request whose branch touched the same item as the target branch therefore shows a conflict, and the merge button stays locked. What you would be offered there are conflict markers inside an item file, which is no place to decide anything.

The CI file joy writes

joy init writes a small CI file for your forge, so the merge happens where joy is installed:

ForgeFile
GitHub.github/workflows/joy-merge.yml
Gitea, Forgejo.gitea/workflows/joy-merge.yml
GitLab.joy/ci/gitlab.yml, included from your .gitlab-ci.yml

Nobody calls anything. On a pull request the job merges the target branch into your branch, resolves the Joy files by Joy's rules and pushes the result. The forge then only has to fast-forward, which cannot fail, and the button turns green. When both sides really changed the same field, the job stops and names the file, because that decision is yours.

joy init ci                      # add it later, for the forge your remote points at
joy init ci --forge github       # or name it: github, gitlab, gitea
joy update                       # brings it to a clone set up before this existed

Joy never touches a file it did not write: a workflow of your own with the same name is left alone, and joy says so.

Contributions from a fork are the one case the job cannot fix: it may only read in someone else's repository. There it comments on the pull request and asks the contributor to merge the target branch in once, which their local joy resolves.

Without CI nothing breaks. Merge the pull request in your clone, the way your forge's own instructions describe; your local joy resolves the item files while doing so.

Cross-Directory Queries (-w)

Joy normally operates on the project containing the current working directory. The global -w / --working-dir <PATH> flag runs a command as if you had cd'd into PATH first:

joy ls -w ../platform            # List items of the sibling project
joy roadmap -w ~/repos/jyn       # Roadmap of an unrelated project
joy log -w ../platform --limit 5 # Audit trail of another tree

PATH must contain a Joy project; otherwise the command bails. Tab completion offers directory names after -w.


Shell Completions

Joy supports tab completion for commands, flags, and item IDs. Add one line to your shell config:

source <(COMPLETE=bash joy)

source <(COMPLETE=zsh joy)

source (COMPLETE=fish joy | psub)

After reloading your shell:

joy show CB-<TAB>                # Completes item and milestone IDs
joy sta<TAB>                     # Completes subcommands
joy ls --ty<TAB>                 # Completes flags

Machine-Readable Output

Every command accepts a global --json flag. Default output stays human-readable; --json switches to a stable, structured envelope so scripts and CI never have to scrape display text:

joy ls --json                                # Same as joy --json ls
joy show JOY-0001 --json                     # Single item as JSON
joy --json ls | jq '.data.items[].id'        # Pipe into jq

The shape is {"version": 1, "data": ...}. Within a major Joy release, fields are added but never removed or repurposed - consumers can rely on the keys they already use. CI scripts should always consume --json, not display output.


Command Reference

CommandWhat it does
joy initInitialize or onboard into a project
joy add <TYPE> <TITLE>Create an item
joy lsList and filter items
joy ls -JList open jobs (-Ja includes closed ones)
joyBoard overview
joy boardBoard view with filters
joy show <ID>Item detail view
joy edit <ID>Modify an item
joy find <TEXT>Search items by text
joy status <ID> <STATUS>Change item status
joy approve/start/stop/submit/rework/close/defer <ID>Status shortcuts
joy reopen <ID>Reopen a closed/deferred item
joy rm <ID>Delete an item
joy assign <ID> [MEMBER]Assign item to member
joy comment <ID> [TEXT]Add comment (opens $EDITOR if TEXT omitted)
joy comment edit <ID> <N> [TEXT]Replace comment #N
joy comment rm <ID> <N> [--force]Delete comment #N
joy deps <ID>Manage dependencies
joy milestoneManage milestones
joy roadmapMilestone roadmap (tree view)
joy logEvent log (audit trail)
joy release bump <BUMP>Step 1: patch version strings in configured files
joy release record <BUMP>Step 2: record, commit, tag (local only)
joy release publishStep 3: push + create the forge release
joy release show [VERSION]Show a release or preview the next
joy release lsList all releases
joy forge loginSign in to a forge (--host, --token-stdin, --for, --login)
joy forge status / joy forge logoutShow or remove the credential this machine holds
joy forge pluginsWhich connector binary answers for which forge
joy projectView/edit project info and members
joy project get/set <KEY> [VALUE]Read or write a project field (e.g. forge, language, docs.*)
joy project member add/edit/show/rm/eraseManage members, capabilities, and interaction levels
joy configShow or modify configuration
joy auth / joy deauthStart or end a session; auth token add issues a delegation token
joy cryptCrypt zones, grants, and confidential items
joy ai initSet up AI tool integration
joy ai tutorialThe operational guide for AI members
joy chat lsList your chats
joy chat add/leave/rename/deleteCreate and manage chats
joy chat show <CHAT>Read a chat and mark it read (--unread, --last N, --since 2h)
joy chat send <CHAT> <TEXT>Send a message
joy chat info <CHAT>Participants and read state
joy updateUpdate the joy binary and refresh joy-managed state
joy update --checkRead-only audit of every joy-managed artefact
joy tutorialYou are here

Every command accepts the global -w / --working-dir <PATH> flag to run as if started from PATH, and the global --session <CREDENTIAL> flag to pass an AI delegation session inline (alternative to JOY_SESSION).

See also: joy --help, joy <command> --help