mintree 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,12 +4,12 @@
4
4
 
5
5
  mintree wraps the steps you do manually every time a feature begins:
6
6
 
7
- 1. Pick an open issue or work item assigned to you (GitHub Issues or Plane).
7
+ 1. Pick an open issue or work item assigned to you (GitHub Issues or Linear).
8
8
  2. Create a git worktree on a branch named after that item, following the project's convention.
9
9
  3. Launch Claude Code inside the worktree with a session ID you can resume later.
10
10
  4. Live-track which Claude sessions are active, idle, or waiting.
11
11
 
12
- It is a smaller, opinionated cousin of [santree](https://github.com/santiagotoscanini/santree) — built on the same TypeScript + Ink + Pastel stack but stripped to the `<type>/<issue>-<desc>` branch convention and the two issue trackers most likely to be used by a small team: GitHub Issues and Plane.
12
+ It is a smaller, opinionated cousin of [santree](https://github.com/santiagotoscanini/santree) — built on the same TypeScript + Ink + Pastel stack but stripped to the `<type>/<issue>-<desc>` branch convention and the two issue trackers most likely to be used by a small team: GitHub Issues and Linear.
13
13
 
14
14
  ---
15
15
 
@@ -48,7 +48,7 @@ EOF
48
48
 
49
49
  `mintree doctor` should report **all required checks pass** before you continue. The most common gaps are:
50
50
 
51
- - `gh` not authenticated → `gh auth login` (still needed when using Plane — mintree uses `gh` for PR status on worktree branches)
51
+ - `gh` not authenticated → `gh auth login` (still needed when using Linear — mintree uses `gh` for PR status on worktree branches)
52
52
  - Claude Code not installed → `npm install -g @anthropic-ai/claude-code`
53
53
  - Shell integration not loaded → re-run the `echo … >> ~/.zshrc` step and start a new shell
54
54
 
@@ -64,8 +64,8 @@ cd path/to/repo
64
64
  # Default: GitHub Issues provider
65
65
  mintree init
66
66
 
67
- # Or: Plane provider
68
- mintree init --provider plane --workspace <your-workspace-slug>
67
+ # Or: Linear provider (--team is repeatable, one per team you pull work from)
68
+ mintree init --provider linear --workspace <your-workspace-slug> --team FE --team BE
69
69
 
70
70
  mintree helpers session-signal install # optional: live session state in the dashboard
71
71
  ```
@@ -77,40 +77,40 @@ mintree helpers session-signal install # optional: live session state in the d
77
77
  mintree supports two issue providers, selected per repo via `.mintree/metadata.json`:
78
78
 
79
79
  - **`github`** (default): lists issues assigned to you on the current repo via `gh`. Transitions to "In Progress" on a Projects v2 board when present.
80
- - **`plane`**: lists work items assigned to you across a configured set of [Plane](https://plane.so) projects, via the Plane REST API. Transitions to the project's "started" state on `w`.
80
+ - **`linear`**: lists issues assigned to you across a configured set of [Linear](https://linear.app) teams, via the Linear GraphQL API. Moves the issue to "In Progress" on `w`.
81
81
 
82
- After `mintree init --provider plane`, edit `.mintree/metadata.json` to add at least one project:
82
+ `mintree init --provider linear --workspace <slug> --team FE --team BE` scaffolds the metadata for you. If you skip `--team`, or want to tweak it later, edit `.mintree/metadata.json` so `linear.teams` lists at least one team key:
83
83
 
84
84
  ```json
85
85
  {
86
86
  "version": 1,
87
- "provider": "plane",
87
+ "provider": "linear",
88
88
  "issues": {},
89
- "plane": {
90
- "apiUrl": "https://api.plane.so",
89
+ "linear": {
90
+ "apiUrl": "https://api.linear.app/graphql",
91
91
  "workspaceSlug": "my-team",
92
- "projects": [
93
- { "id": "<project-uuid>", "identifier": "BACK", "name": "Backend" },
94
- { "id": "<project-uuid>", "identifier": "WEB", "name": "Web" }
92
+ "teams": [
93
+ { "key": "FE", "name": "Frontend" },
94
+ { "key": "BE", "name": "Backend" }
95
95
  ]
96
96
  }
97
97
  }
98
98
  ```
99
99
 
100
- You can find each project's UUID in the Plane URL (`app.plane.so/<workspace>/projects/<uuid>/...`). The `identifier` is the short prefix shown on work-item IDs (the `BACK` in `BACK-100`).
100
+ The `workspaceSlug` is the URL key of your Linear workspace (`linear.app/<slug>/...`). Each team `key` is the short prefix shown on issue IDs (the `FE` in `FE-123`); `name` is optional. Optional keys: `inProgressStateName` (override the workflow state `w` transitions to) and `protectedStateTypes` (workflow-state types `clean` won't touch).
101
101
 
102
- Authenticate by setting `PLANE_API_KEY` in your shell, or by writing the key to `~/.mintree/credentials.json`:
102
+ Authenticate by setting `LINEAR_API_KEY` in your shell, or by writing the key to `~/.mintree/credentials.json`:
103
103
 
104
104
  ```bash
105
- export PLANE_API_KEY=plane_api_XXXXXXXXXXXXXX
105
+ export LINEAR_API_KEY=lin_api_XXXXXXXXXXXXXX
106
106
  # or
107
107
  cat > ~/.mintree/credentials.json <<'EOF'
108
- { "plane": { "apiKey": "plane_api_XXXXXXXXXXXXXX" } }
108
+ { "linear": { "apiKey": "lin_api_XXXXXXXXXXXXXX" } }
109
109
  EOF
110
110
  chmod 600 ~/.mintree/credentials.json
111
111
  ```
112
112
 
113
- `mintree doctor` validates the key + each configured project's reachability when `provider === "plane"`.
113
+ The key goes straight into the `Authorization` header (no `Bearer` prefix). `mintree doctor` validates the key, resolves the viewer, and pings each configured team when `provider === "linear"`.
114
114
 
115
115
  ---
116
116
 
@@ -143,10 +143,11 @@ Same building blocks, scriptable from any shell:
143
143
  ```bash
144
144
  # Create a worktree, optionally launch Claude with an initial prompt
145
145
  mintree worktree create feat/100-validar-patente
146
- mintree worktree create feat/BACK-100-validar-patente --work --prompt "empezar BACK-100"
146
+ mintree worktree create feat/FE-123-validar-patente --work --prompt "empezar FE-123"
147
147
 
148
148
  # Resume Claude in the worktree you're currently inside
149
- cd .mintree/worktrees/BACK-100-validar-patente
149
+ # (the worktree dir is the bare issue id)
150
+ cd .mintree/worktrees/FE-123
150
151
  mintree worktree work
151
152
 
152
153
  # Inspect / clean up
@@ -176,11 +177,11 @@ mintree enforces:
176
177
  `<issue>` is one of:
177
178
 
178
179
  - Bare digits for GitHub Issues (the issue number, no `#`): `42`, `100`, `1234`
179
- - `<PROJ>-<digits>` for Plane work items (the human identifier): `BACK-100`, `WEB-7`, `PROJ_X-12`. The prefix is uppercase letters / digits / underscores, matching Plane's project-identifier constraints.
180
+ - `<TEAM>-<digits>` for Linear issues (the human identifier): `FE-123`, `BE-7`, `DSGN-12`. The prefix is uppercase letters / digits / underscores, matching Linear's team-key constraints.
180
181
 
181
182
  `<desc>` is lowercase kebab-case.
182
183
 
183
- Examples: `feat/42-validacion-patente`, `fix/55-selfie-upload-timeout`, `feat/BACK-100-readme-update`, `fix/WEB-7-modal`.
184
+ Examples: `feat/42-validacion-patente`, `fix/55-selfie-upload-timeout`, `feat/FE-123-readme-update`, `fix/BE-7-modal`.
184
185
 
185
186
  When the dashboard's `w` overlay opens, it suggests a kebab description capped at 5 words. If your repo has a `docs/conventions/git-workflow.md`, `CONTRIBUTING.md`, or `.claude/skills/` directory, mintree mentions it on the overlay so you can verify the suggestion against your project's rules — then edit the description to match.
186
187
 
@@ -194,16 +195,18 @@ When the dashboard's `w` overlay opens, it suggests a kebab description capped a
194
195
  └── .mintree/
195
196
  ├── metadata.json # gitignored. provider config + <issue-id> → { base_branch?, session_id? }
196
197
  ├── worktrees/ # gitignored
197
- │ ├── 100-validar-patente/ # GH form: <digits>-<desc>
198
- │ └── BACK-100-readme-update/ # Plane form: <PROJ-digits>-<desc>
198
+ │ ├── 100/ # GH form: <digits>
199
+ │ └── FE-123/ # Linear form: <TEAM-digits>
199
200
  ├── session-states/ # gitignored
200
201
  │ └── 100.json # live state written by Claude hooks (active/waiting/idle/exited)
201
202
  └── init.sh # opt-in. Runs in the new worktree post-create (copy .env, install deps, …)
202
203
  ```
203
204
 
204
- `metadata.json` is gitignored because the `session_id` is local to your machine sharing it would only generate noise. The `provider` and `plane.*` keys can be re-derived from a Plane workspace if needed; sharing them would just leak local config preference.
205
+ The worktree directory is named after the bare issue id (`100`, `FE-123`); the branch keeps the full `<type>/<issue>-<desc>` name.
205
206
 
206
- Plane authentication lives in `~/.mintree/credentials.json` (user-scoped, not per-repo) or the `PLANE_API_KEY` env var.
207
+ `metadata.json` is gitignored because the `session_id` is local to your machine — sharing it would only generate noise. The `provider` and `linear.*` keys can be re-derived from a Linear workspace if needed; sharing them would just leak local config preference.
208
+
209
+ Linear authentication lives in `~/.mintree/credentials.json` (user-scoped, not per-repo) or the `LINEAR_API_KEY` env var.
207
210
 
208
211
  ---
209
212
 
@@ -220,7 +223,7 @@ Plane authentication lives in `~/.mintree/credentials.json` (user-scoped, not pe
220
223
  - `mintree doctor` is the first stop. It surfaces missing tools, unauthenticated CLIs, missing hooks, and gitignore drift.
221
224
  - The shell wrapper exports `MINTREE_SHELL_INTEGRATION=1` — if doctor says it's missing, the wrapper isn't being loaded by your shell init file.
222
225
  - If the dashboard ever opens with a stale session state, press `r` to force a refetch (the auto-refresh runs every 5 minutes).
223
- - Plane-side issues (timeouts, rate limits, unexpected response shapes) can be logged to `~/.mintree/plane-debug.log` by running `MINTREE_DEBUG=1 mintree dashboard`. The log is file-only so it never corrupts the Ink-rendered TUI.
226
+ - Linear-side issues (timeouts, rate limits, unexpected response shapes) can be logged to `~/.mintree/linear-debug.log` by running `MINTREE_DEBUG=1 mintree dashboard`. The log is file-only so it never corrupts the Ink-rendered TUI.
224
227
 
225
228
  ---
226
229
 
@@ -228,7 +231,7 @@ Plane authentication lives in `~/.mintree/credentials.json` (user-scoped, not pe
228
231
 
229
232
  mintree was written for projects that have:
230
233
 
231
- - A small set of trackers (GitHub Issues or Plane) — santree supports both Linear and GitHub but is heavier.
234
+ - A small set of trackers (GitHub Issues or Linear) — santree supports both too but is heavier.
232
235
  - An established branch convention without the `gh-` prefix santree imposes.
233
236
  - Skills (`.claude/skills/`) that own the SDD + TDD flow — mintree intentionally leaves the rich PR-create / PR-review prompts out of scope.
234
237
 
@@ -23,11 +23,19 @@ export const description = "Interactive dashboard listing open issues assigned t
23
23
  function isOrphan(d) {
24
24
  return d.orphan === true;
25
25
  }
26
- function tabIssues(issues, tab) {
27
- return issues.filter((d) => (tab === "issues" ? !isOrphan(d) : isOrphan(d)));
26
+ // Matches an issue against the live numeric filter by substring on the digit
27
+ // portion of its id ("PLA-234" "234", "BE-34" → "34"). Letters are ignored,
28
+ // so the user filters by ticket number alone. Empty filter matches everything.
29
+ function issueMatchesFilter(d, filter) {
30
+ if (!filter)
31
+ return true;
32
+ return d.issue.id.replace(/\D/g, "").includes(filter);
33
+ }
34
+ function tabIssues(issues, tab, filter = "") {
35
+ return issues.filter((d) => (tab === "issues" ? !isOrphan(d) : isOrphan(d)) && issueMatchesFilter(d, filter));
28
36
  }
29
37
  function currentSelected(s) {
30
- const displayed = tabIssues(s.issues, s.activeTab);
38
+ const displayed = tabIssues(s.issues, s.activeTab, s.filter);
31
39
  const selectedIndex = s.activeTab === "issues" ? s.issuesIndex : s.worktreesIndex;
32
40
  return { displayed, selectedIndex };
33
41
  }
@@ -186,7 +194,7 @@ function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
186
194
  // align under the left (list) pane; ticket-specific actions align under
187
195
  // the right (detail) pane. Falls back to a single inline row when no
188
196
  // width hint is available (e.g. the error path).
189
- const common = (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: " scroll " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " q" }), _jsx(Text, { dimColor: true, children: " quit" })] }));
197
+ const common = (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: " scroll " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " #" }), _jsx(Text, { dimColor: true, children: " filter " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " q" }), _jsx(Text, { dimColor: true, children: " quit" })] }));
190
198
  const ticket = (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "\u21B5" }), _jsx(Text, { dimColor: true, children: " Switch " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " w" }), _jsx(Text, { dimColor: true, children: " Work " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " o" }), _jsx(Text, { dimColor: true, children: " Open " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " d" }), _jsx(Text, { dimColor: true, children: " Remove" })] }));
191
199
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: listWidth, children: common }), _jsx(Box, { flexGrow: 1, children: ticket })] }), latestVersion && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "(*)" }), _jsx(Text, { dimColor: true, children: ` new version available — v${latestVersion} · npm i -g mintree` })] }))] }));
192
200
  }
@@ -674,14 +682,15 @@ export default function Dashboard() {
674
682
  const previousOverlay = prevReady?.overlay ?? null;
675
683
  const previousToast = prevReady?.toast ?? null;
676
684
  const previousScroll = prevReady?.detailScrollOffset ?? 0;
677
- const issuesList = tabIssues(issues, "issues");
678
- const worktreesList = tabIssues(issues, "worktrees");
685
+ const filter = prevReady?.filter ?? "";
686
+ const issuesList = tabIssues(issues, "issues", filter);
687
+ const worktreesList = tabIssues(issues, "worktrees", filter);
679
688
  const issuesIndex = Math.min(previousIssuesIndex, Math.max(0, issuesList.length - 1));
680
689
  const worktreesIndex = Math.min(previousWorktreesIndex, Math.max(0, worktreesList.length - 1));
681
690
  // Preserve scroll only when the active tab's selected issue still
682
691
  // resolves to the same row — clamping or list churn means the user
683
692
  // is now reading something else.
684
- const prevDisplayed = prevReady ? tabIssues(prevReady.issues, activeTab) : [];
693
+ const prevDisplayed = prevReady ? tabIssues(prevReady.issues, activeTab, filter) : [];
685
694
  const nextDisplayed = activeTab === "issues" ? issuesList : worktreesList;
686
695
  const prevSelectedId = prevDisplayed[activeTab === "issues" ? previousIssuesIndex : previousWorktreesIndex]?.issue
687
696
  .id ?? null;
@@ -697,6 +706,7 @@ export default function Dashboard() {
697
706
  refreshing: false,
698
707
  overlay: previousOverlay,
699
708
  toast: previousToast,
709
+ filter,
700
710
  };
701
711
  });
702
712
  };
@@ -818,12 +828,42 @@ export default function Dashboard() {
818
828
  handleOverlayInput(input, key);
819
829
  return;
820
830
  }
831
+ // Esc clears an active numeric filter before it falls through to quit —
832
+ // so the user can back out of a search without leaving the dashboard.
833
+ if (key.escape && state.phase === "ready" && state.filter) {
834
+ setState({ ...state, filter: "", issuesIndex: 0, worktreesIndex: 0, detailScrollOffset: 0 });
835
+ return;
836
+ }
821
837
  if (input === "q" || key.escape || (input === "c" && key.ctrl)) {
822
838
  exit();
823
839
  return;
824
840
  }
825
841
  if (state.phase !== "ready")
826
842
  return;
843
+ // Numeric filter: typing a digit narrows the list by ticket number
844
+ // (matched on the digits of the id, so "34" hits both PLA-234 and BE-34).
845
+ // Backspace pops a digit; Esc (handled above) clears it. Reset selection
846
+ // to the first match so the cursor stays on a visible row as it narrows.
847
+ if (/^[0-9]$/.test(input)) {
848
+ setState({
849
+ ...state,
850
+ filter: state.filter + input,
851
+ issuesIndex: 0,
852
+ worktreesIndex: 0,
853
+ detailScrollOffset: 0,
854
+ });
855
+ return;
856
+ }
857
+ if ((key.backspace || key.delete) && state.filter) {
858
+ setState({
859
+ ...state,
860
+ filter: state.filter.slice(0, -1),
861
+ issuesIndex: 0,
862
+ worktreesIndex: 0,
863
+ detailScrollOffset: 0,
864
+ });
865
+ return;
866
+ }
827
867
  if (key.leftArrow || key.rightArrow) {
828
868
  // Two tabs only — either arrow toggles. Per-tab indices are
829
869
  // preserved, so the user returns to the row they left.
@@ -1172,7 +1212,7 @@ export default function Dashboard() {
1172
1212
  if (state.phase === "error") {
1173
1213
  return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", state.message] }), state.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", state.hint] }) })), _jsx(Box, { marginTop: 1, children: _jsx(FooterRow, { phase: "error" }) })] }));
1174
1214
  }
1175
- const { issues, refreshing, overlay, toast, activeTab } = state;
1215
+ const { issues, refreshing, overlay, toast, activeTab, filter } = state;
1176
1216
  const { displayed, selectedIndex } = currentSelected(state);
1177
1217
  const selected = displayed[selectedIndex] ?? null;
1178
1218
  const issuesTabCount = issues.reduce((n, d) => (isOrphan(d) ? n : n + 1), 0);
@@ -1224,7 +1264,9 @@ export default function Dashboard() {
1224
1264
  : displayed.map((d, index) => ({ kind: "issue", d, index }));
1225
1265
  const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
1226
1266
  const listContentWidth = Math.max(8, listWidth - 4);
1227
- return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount: issuesTabCount, worktreeCount: worktreesTabCount, activeTab: activeTab, updateAvailable: latestVersion !== null }) }), overlay ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", borderStyle: "round", borderColor: overlay.kind === "remove" ? "yellow" : "cyan", children: overlay.kind === "create" ? (_jsx(CreateOverlayView, { overlay: overlay, onDescChange: onOverlayDescChange, onPromptChange: onOverlayPromptChange })) : (_jsx(RemoveOverlayView, { overlay: overlay })) })) : (_jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsx(Box, { width: listWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: displayed.length === 0 ? (_jsx(Text, { dimColor: true, children: activeTab === "issues"
1228
- ? "No open issues assigned to you in this repo."
1229
- : "No orphaned worktrees — anything in `.mintree/worktrees/` matches an open issue." })) : (_jsxs(_Fragment, { children: [listView.sticky.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `sticky-${i}`))), listView.issuesAbove > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2191 ", listView.issuesAbove, " more above"] })), listView.body.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `body-${i}`))), listView.issuesBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", listView.issuesBelow, " more below"] }))] })) }), _jsx(Box, { width: detailWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(DetailPane, { d: selected, contentWidth: detailWidth - 4, contentHeight: detailContentHeight, scrollOffset: state.detailScrollOffset }) })] })), _jsxs(Box, { paddingX: 1, flexDirection: "column", children: [toast && (_jsx(Box, { children: _jsxs(Text, { color: toast.kind === "success" ? "green" : toast.kind === "error" ? "red" : "cyan", children: [toast.kind === "success" ? "✓ " : toast.kind === "error" ? "✗ " : "· ", toast.text] }) })), refreshing && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " refreshing" })] })), _jsx(FooterRow, { phase: "ready", overlayKind: overlay?.kind, latestVersion: latestVersion, listWidth: listWidth })] })] }));
1267
+ return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount: issuesTabCount, worktreeCount: worktreesTabCount, activeTab: activeTab, updateAvailable: latestVersion !== null }) }), overlay ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", borderStyle: "round", borderColor: overlay.kind === "remove" ? "yellow" : "cyan", children: overlay.kind === "create" ? (_jsx(CreateOverlayView, { overlay: overlay, onDescChange: onOverlayDescChange, onPromptChange: onOverlayPromptChange })) : (_jsx(RemoveOverlayView, { overlay: overlay })) })) : (_jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsx(Box, { width: listWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: displayed.length === 0 ? (_jsx(Text, { dimColor: true, children: filter
1268
+ ? `No tickets match #${filter} — Esc to clear the filter.`
1269
+ : activeTab === "issues"
1270
+ ? "No open issues assigned to you in this repo."
1271
+ : "No orphaned worktrees — anything in `.mintree/worktrees/` matches an open issue." })) : (_jsxs(_Fragment, { children: [listView.sticky.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `sticky-${i}`))), listView.issuesAbove > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2191 ", listView.issuesAbove, " more above"] })), listView.body.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `body-${i}`))), listView.issuesBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", listView.issuesBelow, " more below"] }))] })) }), _jsx(Box, { width: detailWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(DetailPane, { d: selected, contentWidth: detailWidth - 4, contentHeight: detailContentHeight, scrollOffset: state.detailScrollOffset }) })] })), _jsxs(Box, { paddingX: 1, flexDirection: "column", children: [filter && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: `⌕ #${filter}` }), _jsx(Text, { dimColor: true, children: ` · ${displayed.length} match${displayed.length === 1 ? "" : "es"} · Esc clear` })] })), toast && (_jsx(Box, { children: _jsxs(Text, { color: toast.kind === "success" ? "green" : toast.kind === "error" ? "red" : "cyan", children: [toast.kind === "success" ? "✓ " : toast.kind === "error" ? "✗ " : "· ", toast.text] }) })), refreshing && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " refreshing" })] })), _jsx(FooterRow, { phase: "ready", overlayKind: overlay?.kind, latestVersion: latestVersion, listWidth: listWidth })] })] }));
1230
1272
  }
@@ -67,15 +67,16 @@ function resolve(cwd) {
67
67
  const worktreeDirName = path.basename(worktreePath);
68
68
  // IssueId comes from the worktree dir name, not the branch — that way
69
69
  // detached-HEAD worktrees (the "current branch" path from the dashboard)
70
- // still resolve their session_id. Convention guarantees the dir is named
71
- // `<issueId>-<desc>` for both attached and detached creates, where
72
- // issueId is either bare digits (GitHub) or `<TEAM>-\d+` (Linear).
73
- const issueIdMatch = worktreeDirName.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)-/);
70
+ // still resolve their session_id. The dir is named after the bare issue
71
+ // id for both attached and detached creates; the trailing `-` clause still
72
+ // matches legacy `<issueId>-<desc>` worktrees. issueId is either bare
73
+ // digits (GitHub) or `<TEAM>-\d+` (Linear).
74
+ const issueIdMatch = worktreeDirName.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)(?:-|$)/);
74
75
  if (!issueIdMatch || !issueIdMatch[1]) {
75
76
  return {
76
77
  ok: false,
77
78
  message: `Worktree directory '${worktreeDirName}' doesn't start with an issue id.`,
78
- hint: "Expected `<issue>-<desc>` (e.g. 100-claude-md-inicial or AUTH-6-legal-endpoint).",
79
+ hint: "Expected the issue id (e.g. 100 or AUTH-6).",
79
80
  };
80
81
  }
81
82
  const issueId = issueIdMatch[1];
@@ -29,7 +29,7 @@ export const ALLOWED_TYPES = [
29
29
  // `<TEAM_PREFIX>-\d+` (Linear). The TEAM_PREFIX is uppercase letters/digits/
30
30
  // underscores starting with a letter, mirroring Linear's team-key
31
31
  // constraints. The full issueId captures group 2 verbatim so callers can
32
- // round-trip it into the worktree dir name.
32
+ // reuse it as the worktree dir name.
33
33
  const BRANCH_REGEX = /^([a-z]+)\/((?:[A-Z][A-Z0-9_]*-)?\d+)-([a-z0-9][a-z0-9-]*)$/;
34
34
  export function parseBranch(branch) {
35
35
  const match = BRANCH_REGEX.exec(branch);
@@ -57,7 +57,9 @@ export function parseBranch(branch) {
57
57
  type: type,
58
58
  issueId,
59
59
  desc,
60
- worktreeDirName: `${issueId}-${desc}`,
60
+ // Worktree dir is the bare issue id (e.g. "100" or "FE-123"). The desc
61
+ // only seeds the branch name, not the directory.
62
+ worktreeDirName: issueId,
61
63
  };
62
64
  }
63
65
  export function isParseError(result) {
@@ -16,8 +16,10 @@ import { createProvider } from "./providers/index.js";
16
16
  function buildWorktreeIndex(repoRoot) {
17
17
  const worktreesRoot = path.resolve(getWorktreesDir(repoRoot));
18
18
  // Same shape as the BRANCH_REGEX issueId capture: bare digits (GitHub) or
19
- // `<TEAM>-\d+` (Linear). Matches `100-foo` and `FE-123-foo` alike.
20
- const dirNameRegex = /^((?:[A-Z][A-Z0-9_]*-)?\d+)-/;
19
+ // `<TEAM>-\d+` (Linear). The dir name is now the bare issue id (`100`,
20
+ // `FE-123`); the trailing `-` clause keeps matching legacy `<id>-<desc>`
21
+ // worktrees still on disk.
22
+ const dirNameRegex = /^((?:[A-Z][A-Z0-9_]*-)?\d+)(?:-|$)/;
21
23
  const index = new Map();
22
24
  for (const w of listWorktrees(repoRoot)) {
23
25
  const wAbs = path.resolve(w.path);
@@ -119,9 +121,10 @@ function sortGroupedIssues(issues, configuredUrl) {
119
121
  * Worktrees" at the bottom of the dashboard so the user can find and `d`elete
120
122
  * them.
121
123
  *
122
- * The `issue` stub uses the worktree directory name as the title (the part
123
- * after the issue id, e.g. "claude-md-inicial") so the row is identifiable
124
- * even when there's no live issue to fetch a title from.
124
+ * The `issue` stub uses the worktree directory name as the title the bare
125
+ * issue id (e.g. "FE-123"), or the legacy "<id>-<desc>" suffix for older
126
+ * worktrees — so the row is identifiable even when there's no live issue to
127
+ * fetch a title from.
125
128
  */
126
129
  function buildOrphanRows(worktreesByIssue, assignedIds, sessionLookup, prByBranch, metadataSessionId) {
127
130
  const orphans = [];
@@ -16,10 +16,12 @@ export declare function extractRepoAndDir(cwd: string): {
16
16
  worktreeDir: string;
17
17
  } | null;
18
18
  /**
19
- * Pulls the issue id out of a `<issue>-<desc>` worktree directory name.
20
- * Returns null when the directory name doesn't follow the convention (e.g.
21
- * a manually-created worktree dropped under .mintree/worktrees/). The id
22
- * is either bare digits (GitHub) or a `<TEAM>-\d+` Linear identifier.
19
+ * Pulls the issue id out of a worktree directory name. The dir name is the
20
+ * bare issue id (`100`, `FE-123`); the trailing `-` clause still matches
21
+ * legacy `<issue>-<desc>` worktrees on disk. Returns null when the directory
22
+ * name doesn't follow the convention (e.g. a manually-created worktree
23
+ * dropped under .mintree/worktrees/). The id is either bare digits (GitHub)
24
+ * or a `<TEAM>-\d+` Linear identifier.
23
25
  */
24
26
  export declare function issueIdFromWorktreeDir(worktreeDir: string): string | null;
25
27
  export type StatePayload = {
@@ -32,13 +32,15 @@ export function extractRepoAndDir(cwd) {
32
32
  return { repoRoot, worktreeDir };
33
33
  }
34
34
  /**
35
- * Pulls the issue id out of a `<issue>-<desc>` worktree directory name.
36
- * Returns null when the directory name doesn't follow the convention (e.g.
37
- * a manually-created worktree dropped under .mintree/worktrees/). The id
38
- * is either bare digits (GitHub) or a `<TEAM>-\d+` Linear identifier.
35
+ * Pulls the issue id out of a worktree directory name. The dir name is the
36
+ * bare issue id (`100`, `FE-123`); the trailing `-` clause still matches
37
+ * legacy `<issue>-<desc>` worktrees on disk. Returns null when the directory
38
+ * name doesn't follow the convention (e.g. a manually-created worktree
39
+ * dropped under .mintree/worktrees/). The id is either bare digits (GitHub)
40
+ * or a `<TEAM>-\d+` Linear identifier.
39
41
  */
40
42
  export function issueIdFromWorktreeDir(worktreeDir) {
41
- const m = worktreeDir.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)-/);
43
+ const m = worktreeDir.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)(?:-|$)/);
42
44
  return m && m[1] ? m[1] : null;
43
45
  }
44
46
  /**
@@ -66,7 +66,7 @@ export type CreateDetachedOpts = {
66
66
  * the `<type>/<issue>-<desc>` convention upfront. They can `git switch -c`
67
67
  * later if/when the work warrants a branch.
68
68
  *
69
- * Worktree dir naming follows the same `<issueId>-<desc>` shape as the
69
+ * Worktree dir naming follows the same bare-issueId shape as the
70
70
  * branch-based flow so `worktree work` can still recover the issueId from
71
71
  * the dir name (where it can't read it from the branch).
72
72
  */
@@ -238,7 +238,7 @@ export async function runCreate(branchArg, opts) {
238
238
  * the `<type>/<issue>-<desc>` convention upfront. They can `git switch -c`
239
239
  * later if/when the work warrants a branch.
240
240
  *
241
- * Worktree dir naming follows the same `<issueId>-<desc>` shape as the
241
+ * Worktree dir naming follows the same bare-issueId shape as the
242
242
  * branch-based flow so `worktree work` can still recover the issueId from
243
243
  * the dir name (where it can't read it from the branch).
244
244
  */
@@ -280,7 +280,7 @@ export async function runCreateDetached(opts) {
280
280
  hint: "Switch the main repo to a branch first (`git switch main`) and try again.",
281
281
  };
282
282
  }
283
- const worktreeDirName = `${opts.issueId}-${opts.descKebab}`;
283
+ const worktreeDirName = opts.issueId;
284
284
  const worktreePath = path.join(getWorktreesDir(root), worktreeDirName);
285
285
  if (pathExists(worktreePath)) {
286
286
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mintree",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Issue-driven git worktrees + Claude Code sessions for repos with an opinionated SDD+TDD flow.",
5
5
  "license": "MIT",
6
6
  "author": "Martin Mineo <mmineo@canarytechnologies.com>",