A multi-repository task management system for Neovim that integrates Jira workflows with git worktrees and session management.
Here's how a typical workflow looks:
-
Navigate to your tasks directory:
cd ~/tasks nvim
-
Pick a Jira issue: A picker automatically opens showing your assigned issues. Select one (or choose manual entry). (If Jira integration is disabled, you'll drop straight into the manual entry flow.)
-
Confirm the branch suffix: The issue summary is automatically converted to a branch suffix (e.g., "Fix Login Bug" →
fix-login-bug). Edit or accept it. -
Select a repository: Choose which repo to start with from your available bare repositories.
-
Review the issue brief:
issue-description.mdis created alongside your worktrees. It contains the Jira summary/description so you always have the ticket context locally (see the samplekeymapsin the config section if you want to change the shortcuts referenced below). -
Start working: You're now in
~/tasks/dev-123-fix-login-bug/repo-name/with the branchdev-123-fix-login-bugchecked out.
Need to work on another repo for the same ticket? With the default keymaps from the config snippet below, press <leader>ta to add more repositories to the task.
If you use Sidekick with Claude Code (recommended in the sample config), the claude CLI automatically starts from the task directory. Pop it open and point it at the description:
" inside the task
:edit issue-description.md " skim or update the brief
<leader>ac " toggle Claude Code via Sidekick (see config snippet)From the Claude prompt you can run:
take a look at @issue-description.md and give me a plan for this task
Because the CLI launches in the task root, it immediately sees issue-description.md, your repo worktrees, and any notes you add while working.
This task manager helps you organize your work when a single Jira ticket requires changes across multiple repositories. It creates isolated workspaces for each task using git worktrees.
~/
├── repos/ # Bare git repositories
│ ├── repo1/ # Bare repo (git clone --bare)
│ ├── repo2/ # Bare repo
│ └── repo3/ # Bare repo
└── tasks/ # Task workspaces
├── dev-123/ # Task directory (one per Jira issue)
│ ├── repo1/ # Git worktree for repo1
│ └── repo2/ # Git worktree for repo2
└── dev-456/
└── repo3/
Each task directory is a separate Claude Code session, allowing you to maintain context per Jira ticket.
- Multi-repo support: Work on multiple repositories for a single Jira ticket
- Git worktrees: Isolated branches per task without switching in your main repos
- Automatic session management: Sessions restore automatically when you cd into a task
- Jira issue picker: Fuzzy-select any open Jira issue assigned to you (with manual entry fallback)
- Interactive prompts: Consistent Snacks.nvim inputs throughout the workflow
- Telescope integration: Quick navigation between tasks and repositories
- Unified picker: Manage existing tasks, create new ones, or clean stale worktrees from the Telescope UI (press on a task entry to delete it).
- Worktree cleanup tools: Commands to prune stale git worktrees and remove task directories safely.
- Contextual hooks: Auto-open whatever tooling you like (CodeCompanion, Neotree, etc.) when you jump into a task workspace via configurable callbacks.
Add this spec to your Lazy config.
{
"bartcortooms/task-manager.nvim",
dependencies = {
"folke/snacks.nvim",
"grapp-dev/nui-components.nvim",
"MunifTanjim/nui.nvim",
"nvim-telescope/telescope.nvim",
-- Optional Jira integration (see below)
-- "janBorowy/jirac.nvim",
},
config = function()
require("task-manager").setup({
jira_prefix = "DEV",
jira = {
url = "https://company.atlassian.net",
email = "you@company.com",
api_token = function()
return vim.fn.system("security find-generic-password -s 'Jira API Token' -w")
end,
},
})
end,
}folke/snacks.nvim(inputs, pickers)- (Optional)
janBorowy/jirac.nvim+nvim-lua/plenary.nvim(Jira REST client backing the issue picker) grapp-dev/nui-components.nvim+MunifTanjim/nui.nvim(menus)nvim-telescope/telescope.nvim(task picker)- Optional integrations:
nvim-neo-tree/neo-tree.nvim,rmagatti/auto-session
Convert your existing repos to bare clones:
# Create the repos directory
mkdir -p ~/repos
# For each repository you work with:
cd ~/repos
git clone --bare git@github.com:username/repo-name.git repo-nameThe task manager is configured in lua/plugins/init.lua:
require("task-manager").setup({
jira_prefix = "DEV",
jira = {
url = "https://company.atlassian.net",
email = "you@company.com",
api_token = function()
return vim.fn.system("security find-generic-password -s 'Jira API Token' -w")
end,
jql = "assignee = currentUser() AND resolution = Unresolved ORDER BY updated DESC",
max_results = 50,
},
keymaps = {
list_tasks = "<leader>tt",
jira_task = "<leader>tc",
add_repo = "<leader>ta",
list_task_repos = "<leader>tr",
list_prs = "<leader>tpr",
},
-- tasks_base = vim.fn.expand("~") .. "/tasks",
-- repos_base = vim.fn.expand("~") .. "/repos",
-- auto_open = {
-- callbacks = { "session_restore", "neotree", "outline", "codecompanion" },
-- },
})jira.api_token can return a string or execute a secret retrieval function (e.g. macOS Keychain, 1Password CLI, environment variable). All fields are mandatory and validated during setup().
Tip: The
jira.urlvalue can include or omithttps://; it is normalised internally (e.g."https://foobar.atlassian.net/"→"foobar.atlassian.net").
jirac.nvim powers the Jira picker and issue-description sync, but the task manager still works without it (you'll use the manual entry path). To enable Jira:
- Add the dependency to your plugin manager (uncomment the line in the spec above, or add it separately). It requires
nvim-lua/plenary.nvim. - Ensure you provide the Jira credentials in the
jirablock insiderequire("task-manager").setup({ ... }).
The plugin needs three bits of information:
jira.url– your Atlassian Cloud hostname such ashttps://company.atlassian.net(the protocol/ trailing slash are optional; they’re normalised internally).jira.email– the email address tied to your Jira account.jira.api_token– an API token created at https://id.atlassian.com/manage-profile/security/api-tokens.
You can hardcode the token (string) or return it from a function. A few common patterns:
-- macOS Keychain
jira = {
api_token = function()
return vim.fn.system("security find-generic-password -s 'Jira API Token' -w")
end,
}
-- 1Password CLI (op inject)
jira = {
api_token = function()
return vim.fn.system("op read op://Engineering/Jira/api_token --no-newline")
end,
}
-- Environment variable
jira = {
api_token = os.getenv("JIRA_API_TOKEN"),
}Whatever you return is trimmed; if the command fails the plugin will warn you during setup.
When jirac.nvim is missing the plugin falls back to manual creation—no errors, just a heads-up notification when you try to open the picker.
If you're using Sidekick to drive Claude Code, configure the CLI defaults and keymaps explicitly so the shortcuts referenced in the quick start work out of the box:
{
"folke/sidekick.nvim",
opts = {
cli = {
default = "claude",
mux = {
backend = "tmux",
enabled = true,
},
},
},
keys = {
{ "<leader>aa", function() require("sidekick.cli").toggle() end, desc = "Toggle CLI" },
{ "<leader>as", function() require("sidekick.cli").select() end, desc = "Select CLI" },
{ "<leader>ap", function() require("sidekick.cli").prompt() end, desc = "Select Prompt" },
{ "<leader>ac", function() require("sidekick.cli").toggle({ name = "claude" }) end, desc = "Open Claude Code" },
},
}Option 1: From the tasks directory
cd ~/tasks
nvim
# Snacks will prompt you to pick an assigned Jira issue or manual entryOption 2: Using the command
:JiraTask 123
" or
:JiraTask DEV-123Passing an argument skips the picker and opens the manual flow pre-filled with the issue you typed.
Option 3: Using the keybinding
Press <leader>tc to open the Snacks picker. Choose an assigned issue (or the manual option). When an issue is selected its summary is slugified to pre-fill the branch suffix prompt—feel free to edit before confirming.
The task directory name (e.g. dev-123-fix-login) doubles as the git branch name for every worktree in that task. Manual entry prompts first for the issue key, then for the optional suffix.
- Assigned issues first: The picker shows every unresolved Jira issue assigned to you (top entry is always “➕ Manual entry”).
- Manual fallback: Choosing the manual entry flows straight into Snacks input boxes so you can type the issue key (
DEV-123) and an optional suffix. - Auto suffix from summary: If you pick a Jira issue the summary is slugified (e.g. “Fix Login Button” →
fix-login-button) and pre-filled in the suffix field—edit or delete as needed. - Branch & directory naming: Branches/directories are always
<issue>-<slug>(lowercased). Selecting “manual” with no suffix just uses the issue key. - Abort-friendly: Closing any picker/input returns
nilso no directories or worktrees are created until you confirm the suffix prompt.
When working on a task that requires changes in multiple repos:
From within a task directory:
:JiraTaskAddRepo
" or press <leader>taThis will:
- Show a list of available repositories
- Create a worktree at
~/tasks/dev-123-feature/repo-name/ - Check out the branch named
dev-123-feature
List all tasks:
:ListTasks
" or press <leader>ttList repositories within current task:
:ListTaskRepos
" or press <leader>trTip: When you jump into a repo the plugin roots Neo-tree at that worktree so git status indicators stay accurate.
Simply navigate to the task directory and open nvim:
cd ~/tasks/dev-123
nvimYour session (buffers, windows, CodeCompanion chats) will be automatically restored!
Removing a task directory manually leaves the associated bare repo marked as a "prunable" worktree. To keep things tidy:
- Run
:TaskCleanup(or pick "🧹 Clean up stale git worktrees" from:ListTasks) to prune any orphaned worktrees across all bare repositories. - Use
:TaskDelete <task-id>to delete a task, remove its worktrees, and then prune automatically. From the:ListTaskspicker you can hit<C-d>on a task entry to trigger the same flow.
These helpers call git worktree remove --force, so make sure you've committed or stashed any local changes before deleting a task.
The module exposes a couple of helpers you can use in your own config:
local tm = require("task-manager")
if tm.is_tasks_base() then
-- We're sitting in the task picker root
end
if tm.is_in_tasks_tree() then
-- Somewhere inside a task workspace
endBoth helpers accept an optional path argument if you need to check something other than the current working directory.
| Command | Description |
|---|---|
:JiraTask <issue> |
Create a new task workspace |
:JiraTaskAddRepo |
Add another repository to current task |
:ListTasks |
Browse all tasks with Telescope |
:ListTaskRepos |
Browse repositories in current task |
:TaskCleanup |
Prune stale git worktrees for all bare repos |
:TaskDelete [task] |
Remove a task directory and its worktrees |
| Key | Action |
|---|---|
<leader>tt |
List all tasks |
<leader>tc |
Create new Jira task (with prompt) |
<leader>ta |
Add repository to current task |
<leader>tr |
List repositories in current task |
-
Create the task:
cd ~/tasks nvim # Pick the DEV-123 issue from the Snacks picker (or choose manual entry and type "123") # When prompted, accept or edit the default suffix (e.g. change to "add-feature") # Select "api"
-
This creates:
- Directory:
~/tasks/dev-123-add-feature/ - Worktree:
~/tasks/dev-123-add-feature/api/ - Branch:
dev-123-add-feature
- Directory:
-
Add the frontend repo:
:JiraTaskAddRepo # Select "frontend" -
Now you have:
~/tasks/dev-123-add-feature/ ├── api/ (branch: dev-123-add-feature) └── frontend/ (branch: dev-123-add-feature) -
Switch between repos:
- Press
<leader>trto see both repos - Select one to jump to it
- Press
-
Come back tomorrow:
cd ~/tasks/dev-123-add-feature nvim # Everything restores automatically!
The task manager works seamlessly with auto-session.nvim. Each task directory gets its own session, so:
- Buffers are restored per task
- Window layouts persist
- CodeCompanion chat history is maintained
- Each task is completely isolated
- Branch naming: The branch name always matches the task directory name (e.g.
dev-123-feature), making it easy to identify which task a branch belongs to - Cleaning up: When done with a task, use
git worktree removeto clean up worktrees, or just delete the task directory - Task isolation: Each task directory is independent—perfect for Claude Code's directory-based sessions
"Bare repo not found" error:
- Ensure your bare repos are in
~/repos/repo-name/(not~/repos/repo-name/.bare/) - Check that the directory contains
HEADandrefs/(indicators of a bare repo)
Session not restoring:
- Ensure
auto-sessionplugin is loaded - Check that you're opening nvim from within the task directory
No prompt when in ~/tasks:
- Make sure the task manager is properly loaded
- Check
tasks_baseconfiguration matches your directory - Try
:lua print(require('task-manager').config.tasks_base)to verify
You can also manage worktrees manually:
# Create a worktree from bare repo
git --git-dir=~/repos/repo-name worktree add ~/tasks/dev-123/repo-name -b dev-123-feature
# List worktrees for a repo
git --git-dir=~/repos/repo-name worktree list
# Remove a worktree
git --git-dir=~/repos/repo-name worktree remove ~/tasks/dev-123/repo-name- ✅ Multi-repo tasks: One workspace for all repos related to a Jira ticket
- ✅ No branch switching: Each task has its own isolated branches
- ✅ Session persistence: Auto-restore your work environment
- ✅ Claude Code friendly: Directory-based sessions work perfectly
- ✅ Clean separation: Each task is completely independent
- ✅ Simple cleanup: Just delete the task directory when done