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
- Setting Up a Project -
init - Creating Items -
add - Filtering and Searching -
ls,show,find - Managing Dependencies -
deps - Tracking Progress -
status,start,submit,close - Working with Milestones -
milestone - Audit Trail and Releases -
log,release - AI Tool Integration -
ai - Project Configuration -
project,config - Shell Completions
- Machine-Readable Output
- Command Reference
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
joyThat's the whole loop. Read on for the details.
Setting Up a Project
Create a fresh project:
mkdir cookbox && cd cookbox
git init
joy initJoy 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 CBJoy 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 initJoy 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 initCreating 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 3Effort
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
| Type | When to use |
|---|---|
epic | Large initiative grouping multiple items |
story | User-facing functionality ("As a user, I can...") |
task | Technical work, not directly visible to users |
bug | Something is broken |
rework | Refactoring or improvement of existing code |
decision | Architecture or product decision to document |
idea | Not yet refined - just capture it before it escapes |
All items start with status new. Priorities: extreme, critical, high, medium (default), low.
Filtering and Searching
List all items:
joy lsFilter 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 descriptionsTags
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 commentsManaging 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-0005This 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 dependencyJoy 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 submit CB-0005 # Shortcut: set to review
joy close CB-0005 # Shortcut: set to closed
joy reopen CB-0005 # Reopen a closed/deferred itemIf 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 #2joy 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-01Link 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-01Check progress:
joy milestone show CB-MS-01 # Progress, risks, blocked items
joy milestone ls # All milestones with counts
joy roadmap # Full roadmap tree viewChildren 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 --item CB-0005 # Events for a specific item
joy log --limit 50 # Show more entriesEvery 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 [ai:claude@joy 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" # REJECTEDThe 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]" # OKIn multi-repo setups (umbrella with submodules), each subproject has its own acronym. CI can enforce the same rule with: just lint-commits
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 releasejoy 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 (via the gh CLI) has a publish backend.
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 runPreview 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 releasesEditing 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 childrenAI Tool Integration
Joy integrates with AI coding tools so they can manage your backlog alongside you.
joy ai initThis does four things:
- Checks if your project has the Vision, Architecture, and Contributing docs (offers to create templates if missing).
- Bootstraps your authentication inline if
joy auth inithas not run yet, so the whole setup is one passphrase. - Detects your installed AI tools (Claude Code, Qwen Code, Mistral Vibe, GitHub Copilot CLI) and writes their tool-specific instruction files plus the
/joyskill where the tool supports skills. - Registers each detected tool as an
ai:<name>@joymember with attested capabilities.
The tool-specific instruction files are intentionally short: they tell the AI its member ID, the correct Co-Authored-By: trailer for commits, and point it at joy ai tutorial as the operational guide. For AI tools that joy ai init cannot auto-detect (e.g. GitHub Copilot Chat in VS Code, Cursor's built-in chat), register a member by hand:
joy project member add ai:copilot-chat@joyjoy project member add for an ai: ID 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 agents 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 (Trustship), the event log (Traceability), and capabilities (Trustship). Gates (Guardianship), cost tracking (Settlement), and AI dispatch (Orchestration) are covered in the Solutions documentation.
AI Identity
AI tools are registered as project members with an ai: prefix:
joy project member add ai:claude@joy # detected automatically by `joy ai init`
joy project member add ai:copilot-chat@joy # manual entry for chat-only toolsWhen 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:
[ai:claude@joy delegated-by:mac@phoenix.org]AI members have the same capabilities as human members, with one exception: AI members cannot perform manage actions (adding members, changing capabilities, 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 staleSee "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 languageSettable keys: name, description, language. Read-only: acronym, created.
Members and Capabilities
Joy tracks project members and their capabilities. Members are added automatically during joy init (from git config user.email) or manually:
joy project member add pete@phoenix.org
joy project member add ai:claude@joy --capabilities "implement,review"
joy project member show pete@phoenix.org
joy project member rm pete@phoenix.orgJoy defines eleven capabilities across two groups.
Lifecycle capabilities govern what a member can do on items:
| Capability | What it grants |
|---|---|
conceive | Frame a problem and propose direction (typically on idea/epic). |
plan | Break work down: scope, effort, milestones. |
design | Settle the technical approach for an item. |
implement | Write the code or content. |
test | Verify behaviour and add tests. |
review | Approve work from someone else and gate submit -> closed. |
document | Update user- or developer-facing docs. |
Management capabilities govern project-level operations:
| Capability | What it grants |
|---|---|
create | Create new items (joy add). |
assign | Assign items to members (joy assign). |
manage | Add/edit members, change project settings. |
delete | Remove items (joy rm). |
joy project member add defaults to the lifecycle set plus create and assign. manage and delete must be granted explicitly. AI members never get manage even when their entry says so - that is enforced at runtime.
Interaction Levels
Each capability also carries an interaction level that tells AI tools how much autonomy they have. Joy defines five levels, from least to most oversight:
autonomous- work independently; only stop at governance gatessupervised- confirm before irreversible actionscollaborative- propose approach, proceed after confirmationinteractive- present options with rationale, wait for user decisionpairing- step by step, question by question
The effective level for a (member, capability) pair is resolved across four layers, each overriding the previous:
- Project defaults (
.joy/project.defaults.yaml) - ship with sensible defaults per capability (e.g.pairingforconceive,collaborativeforimplement). - Project overrides (
.joy/project.yaml) - per-capability settings the team agrees on for this project. - Personal preference (
.joy/config.yaml) - per-user override undermodes.default, applied to capabilities the project hasn't pinned. - Item override - a single item can request a different level via its
modefield, taking effect only for that item.
Inspect what is in force with:
joy project member show ai:claude@joy # All capabilities, current level + source
joy project member show pete@phoenix.orgThe output's third column shows the level and (in brackets) where it was set. Tools and AI agents read this command and follow the level shown - they do not re-derive it.
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_to_closed:
allow_ai: false # AI members may not close items
in_progress_to_review:
allow_ai: trueToday 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 follows the pattern <from>_to_<to> using the lower-case status names.
Solo to Enterprise
Joy scales to the level of ceremony you actually need:
- Solo: one member,
capabilities: all, nostatus_rules. Runjoy initand 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), tighten interaction levels per capability, setallow_ai: falseon 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 fallbacksView 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 overridejoy 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:
| Setting | Default | What it does |
|---|---|---|
workflow.auto-assign | true | Auto-assign items on joy start |
output.color | auto | Color mode: auto, always, never |
output.emoji | false | Show emoji indicators in output |
output.short | true | Compact list output (abbreviations) |
output.fortune | true | Show occasional quotes in output |
auto-sync | true | Refresh 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 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 envelopeThe 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, Homebrew, 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.
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 treePATH 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 flagsMachine-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 jqThe 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
| Command | What it does |
|---|---|
joy init | Initialize or onboard into a project |
joy add <TYPE> <TITLE> | Create an item |
joy ls | List and filter items |
joy | Board overview |
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 start/submit/close <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 milestone | Manage milestones |
joy roadmap | Milestone roadmap (tree view) |
joy log | Event 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 publish [--forge VALUE] | Step 3: push + create the forge release (auto-detects forge from git remotes; --forge overrides per run) |
joy release show [VERSION] | Show a release or preview the next |
joy release ls | List all releases |
joy project | View/edit project info and members |
joy project get/set <KEY> [VALUE] | Read or write a project field (e.g. forge, language, docs.*) |
joy config | Show or modify configuration |
joy ai init | Set up AI tool integration |
joy update | Update the joy binary and refresh joy-managed state |
joy update --check | Read-only audit of every joy-managed artefact |
joy tutorial | You 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