> ## Documentation Index
> Fetch the complete documentation index at: https://taskgraph-ai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Merging tasks.json

> A Git merge driver that keeps both branches' new and archived tasks, renumbers the incoming ones, and marks real clashes as conflicts

# Merging tasks.json

Two branches that each add a task both call it the next free ID, say `5`. A plain Git merge of `.taskgraph/tasks/tasks.json` then conflicts, and resolving it by hand can easily leave a dependency pointing at the wrong task 5.

TaskGraph ships a Git merge driver for `tasks.json`. Git runs it during `git merge`, `git pull`, `git rebase` and `git cherry-pick`, and it merges the file task by task instead of line by line.

## What it does

* **New tasks on both branches are all kept.** When both branches used the same ID, the incoming branch's task gets the next free ID. The same goes for new subtasks of an existing task.
* **References follow the renumbered task.** On the incoming side, task and subtask `dependencies`, `metadata.dependencySources` keys, `related` / `discovered-from` links (`metadata.taskLinks`) and a subtask's `parentId` are rewritten to the new ID.
* **Only IDs that collide move.** An incoming task whose ID is still free keeps it.
* **Changes to different fields merge cleanly.** One branch sets task 3 to `done` while the other edits its description: both changes land.

It prints one line inside the merge output when it renumbered something:

```
✓ Merged tasks.json; renumbered incoming 2 → 4, 1.2 → 1.3
Auto-merging .taskgraph/tasks/tasks.json
Merge made by the 'ort' strategy.
```

A merge that renumbered nothing prints nothing.

## archive.json

The driver also merges `.taskgraph/tasks/archive.json`, so two branches that both ran `taskgraph archive` don't conflict:

* **Archived tasks from both branches are all kept**, tag by tag. `highestId` takes the higher value, so no archived ID is handed out again.
* **IDs never move.** Tasks may depend on an archived ID, so the same ID on both branches is one task, merged field by field (`archivedAt` takes the earlier time). Two different tasks archived under one ID are reported as a clash on their fields.
* **An ID the incoming branch archived and this branch reused** is a clash. Say the incoming branch added task 9 and archived it while this branch added its own task 9. Both are kept, and the driver marks `archive.json` as conflicted:

```
✗ 1 change to archive.json clashes; kept this branch's value
  9  archived on the incoming branch; this branch has another task 9
Run taskgraph move --from=9 --to=<free ID>, then git add both files
```

Moving this branch's task frees the ID for the archived one. `move` also moves dependencies on task 9 to the new ID, so check any that meant the archived task.

The driver tells the tasks apart by title, and it can only check when Git runs it, which is when both branches changed the file. If only the incoming branch touched `archive.json`, Git takes its version without a merge and the reused ID isn't reported; `taskgraph archive restore 9` then says the ID is already in `tasks.json`.

`archive.json` has a driver name of its own, `taskgraph-archive`, so a teammate whose clone was set up by an older TaskGraph (which only knows `taskgraph`) gets Git's normal text merge for it, as before, until they upgrade and run `taskgraph init --merge-driver`.

The driver reads the other file of the pair from the working tree, which during a merge still holds this branch's version, so the order in which Git merges the two files doesn't matter.

## When both branches change the same field

If both branches set the same field of the same task to different values, the driver can't pick one for you. It:

1. keeps **this branch's** value (the branch you're merging into, or the commit being rebased onto; when this branch deleted the field, the incoming value),
2. still writes a valid `tasks.json` with everything else merged, so `taskgraph` keeps working,
3. lists each clash with the incoming value, and
4. exits with an error, so Git marks the file as conflicted and stops the merge.

```
✗ 1 change to tasks.json clashes; kept this branch's value
  1  status  done · incoming: review
Renumbered incoming 2 → 3
Edit .taskgraph/tasks/tasks.json to choose, then git add it
Auto-merging .taskgraph/tasks/tasks.json
CONFLICT (content): Merge conflict in .taskgraph/tasks/tasks.json
```

There are no conflict markers in the file. Change the listed fields if you want the incoming values (or use `taskgraph set-status` and friends), then `git add .taskgraph/tasks/tasks.json` and finish the merge. The incoming version is also in Git's index: `git show :3:.taskgraph/tasks/tasks.json`.

Some fields never clash:

| Field | Merged as |
| - | - |
| `updatedAt`, `updated`, `lastModified` | the later timestamp |
| `created`, `createdAt`, `archivedAt` | the earlier timestamp |
| `schemaVersion`, the archive's `highestId` | the higher one |
| `dependencies`, `tags` and other lists of plain values | both sides' additions and removals |
| `details` that both sides appended to | both appendices, this branch's first |
| a tag's `taskCount`, `completedCount` | recounted |

A task removed on one branch and changed on the other is kept and reported as a clash. Removed on one branch and untouched on the other, it goes.

Two tasks with the same ID inside one branch's file (left over from a hand merge) aren't merged one by one: the whole task list is reported as a clash and this branch's list is kept.

If one branch's `tasks.json` isn't a JSON object (for example, it was committed with conflict markers in it), the driver leaves this branch's file untouched, says which side couldn't be read, and marks the file as conflicted.

## Setting it up

New projects get it from `taskgraph init` when tasks are stored in Git (the default inside a Git repository). For a project that was set up before, a fresh clone, or a project set up before `archive.json` had a driver, run:

```bash theme={null}
taskgraph init --merge-driver
```

It only sets up the driver and touches nothing else. It is safe to run again; when everything is in place it says so.

```
✓ Git now merges tasks.json by task
  .gitattributes  routes tasks.json and archive.json to it; commit it
  .git/config     turns it on here; each clone runs init --merge-driver
```

The setup has two parts:

| Part | Where | Shared? |
| - | - | - |
| `.taskgraph/tasks/tasks.json merge=taskgraph` and `.taskgraph/tasks/archive.json merge=taskgraph-archive` | `.gitattributes` in the project root | Yes, commit it |
| `merge.taskgraph.driver` and `merge.taskgraph-archive.driver` (the command Git runs) | `.git/config` | **No.** Git never pushes or clones config |

> [!WARNING]
>
> **Each clone runs `taskgraph init --merge-driver` once.** The committed `.gitattributes` line names the driver, but the command behind it lives in each clone's own `.git/config`. A teammate who clones the repository and skips this step gets Git's normal line-by-line merge of `tasks.json`: nothing breaks, but colliding task IDs conflict as before.

### A merge setup of your own

If `.gitattributes` already gives `tasks.json` or `archive.json` a merge attribute (such as `merge=union`), or `.git/config` has a `merge.taskgraph.driver` that TaskGraph didn't write, setup leaves it as it is and says so:

```
! Kept your own tasks.json merge setup
  .git/config     merge.taskgraph.driver is yours; unset it for TaskGraph's
```

Run `git config --unset merge.taskgraph.driver` (or delete the `.gitattributes` line), then `taskgraph init --merge-driver` again.

### Which command Git runs

When `taskgraph` is on your `PATH`, Git runs it by name, so upgrading TaskGraph upgrades the driver. Otherwise (a project-local install or `npx`) Git runs the CLI that ran `init`, by its full path. Rerun `taskgraph init --merge-driver` if you move or reinstall it.

### When TaskGraph isn't installed

The command first checks that the CLI is still there. If it isn't (`taskgraph` was uninstalled, the path moved, or the clone is on a machine without it), Git falls back to its own text merge, `git merge-file`. You get an ordinary conflict with `<<<<<<< ours` / `>>>>>>> theirs` markers, the same as in a repository without the driver. A merge never silently drops the incoming side.

## Not covered

Projects that keep tasks out of Git (`init --no-git-tasks`) don't need the driver, and `init` doesn't set it up.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.