mintree 0.4.10 → 0.5.0
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 +26 -3
- package/dist/commands/dashboard.js +156 -26
- package/dist/commands/orchestrate.d.ts +17 -0
- package/dist/commands/orchestrate.js +154 -0
- package/dist/lib/markers.d.ts +11 -0
- package/dist/lib/markers.js +16 -0
- package/dist/lib/metadata.d.ts +1 -0
- package/dist/lib/metadata.js +5 -0
- package/dist/lib/promptTemplate.d.ts +27 -0
- package/dist/lib/promptTemplate.js +35 -0
- package/dist/lib/worktreeCreate.d.ts +6 -0
- package/dist/lib/worktreeCreate.js +1 -1
- package/package.json +1 -1
- package/shell/init.bash +17 -3
- package/shell/init.zsh +17 -3
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ The key goes straight into the `Authorization` header (no `Bearer` prefix). `min
|
|
|
128
128
|
|
|
129
129
|
### Launch behaviour (optional)
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
Three top-level keys in `.mintree/metadata.json` tune how mintree launches Claude — all apply to GitHub and Linear repos alike:
|
|
132
132
|
|
|
133
133
|
```json
|
|
134
134
|
{
|
|
@@ -137,6 +137,7 @@ Two top-level keys in `.mintree/metadata.json` tune how mintree launches Claude
|
|
|
137
137
|
"issues": {},
|
|
138
138
|
"defaultPermissionMode": "auto",
|
|
139
139
|
"promptTemplate": "Trabajá en el ticket {{id}} ({{title}}). Abrí {{url}} para el contexto completo y seguí las convenciones del repo.",
|
|
140
|
+
"orchestratorPromptTemplate": "Hacé de orquestador con los tickets {{ids}} ({{count}} en total). Resolvelos con la menor intervención posible, paralelizando con subagentes salvo dependencias.",
|
|
140
141
|
"linear": { "workspaceSlug": "my-team", "teams": [{ "key": "FE" }] }
|
|
141
142
|
}
|
|
142
143
|
```
|
|
@@ -151,6 +152,14 @@ Two top-level keys in `.mintree/metadata.json` tune how mintree launches Claude
|
|
|
151
152
|
| `{{url}}` | Issue URL (GitHub issue page / Linear issue link) |
|
|
152
153
|
|
|
153
154
|
It's a single line on purpose — the overlay's prompt field is one line, and you can still edit it before launching. When omitted, mintree falls back to its provider-aware default (`gh issue view` for GitHub, the bare id + URL for Linear).
|
|
155
|
+
- **`orchestratorPromptTemplate`**: the message handed to the Claude **orchestrator** launched from the dashboard's `Orchestrate` tab (or `mintree orchestrate`). It replaces the built-in default and supports:
|
|
156
|
+
|
|
157
|
+
| Placeholder | Replaced with |
|
|
158
|
+
|-------------|--------------------------------------------------------|
|
|
159
|
+
| `{{ids}}` | Comma-separated list of the selected ticket ids |
|
|
160
|
+
| `{{count}}` | How many tickets were selected |
|
|
161
|
+
|
|
162
|
+
When omitted, mintree uses a built-in default that asks Claude to orchestrate the selected tickets with minimal intervention — parallelising via subagents unless dependencies force sequential work, creating a worktree per ticket with mintree, using the repo's skills, and moving each ticket to *in progress* on start and closing it when done.
|
|
154
163
|
|
|
155
164
|
---
|
|
156
165
|
|
|
@@ -164,10 +173,19 @@ mintree dashboard
|
|
|
164
173
|
|
|
165
174
|
Opens a full-screen TUI listing your assigned open issues (or work items), each row marked with the live state of its Claude session (`● active`, `! waiting`, `○ idle`, `— exited`, `· no session`). Rows are grouped by project board and Status. The right pane shows the issue body, labels, worktree info, PR status, and live session message.
|
|
166
175
|
|
|
176
|
+
It has three tabs, switched with `←` / `→`:
|
|
177
|
+
|
|
178
|
+
- **Issues** — your assigned open issues, grouped by project/Status.
|
|
179
|
+
- **Worktrees** — orphaned worktrees (on disk under `.mintree/worktrees/` but no longer matching an open issue).
|
|
180
|
+
- **Orchestrate** — the same issues as the Issues tab, but each row is a checkbox (`[ ]` / `[✔]`). Tick the tickets you want resolved and press `↵` to launch a single Claude **orchestrator** in the repo root that drives them to completion (parallel subagents when possible, sequential otherwise), creating a worktree per ticket with mintree. The message is built from `orchestratorPromptTemplate` (see above) or the built-in default.
|
|
181
|
+
|
|
167
182
|
| Shortcut | Action |
|
|
168
183
|
|----------|-----------------------------------------------------------------------|
|
|
169
|
-
|
|
|
170
|
-
|
|
|
184
|
+
| `←/→` | Switch tab (Issues → Worktrees → Orchestrate) |
|
|
185
|
+
| `↑/↓` or `j/k` | Move between rows |
|
|
186
|
+
| `↵` | Issues/Worktrees: resume Claude in the existing worktree, or open the create overlay. Orchestrate: launch the orchestrator over the checked tickets |
|
|
187
|
+
| `Space` | Orchestrate tab: toggle the ticket under the cursor |
|
|
188
|
+
| `a` | Orchestrate tab: select / deselect all visible tickets |
|
|
171
189
|
| `w` | Always open the create overlay (type + kebab description) |
|
|
172
190
|
| `d` | Delete the selected worktree (confirmation overlay) |
|
|
173
191
|
| `r` | Manual refresh (auto-refreshes silently every 5 min) |
|
|
@@ -199,6 +217,11 @@ mintree worktree list --pr # also fetch PR status per branch
|
|
|
199
217
|
mintree worktree remove fix/55-bug # drop worktree but keep branch + session_id
|
|
200
218
|
mintree worktree remove fix/55-bug --force # discard uncommitted changes too
|
|
201
219
|
mintree worktree clean # sweep worktrees whose PR is merged/closed
|
|
220
|
+
|
|
221
|
+
# Launch a Claude orchestrator over a batch of tickets (renders
|
|
222
|
+
# orchestratorPromptTemplate, or the built-in default)
|
|
223
|
+
mintree orchestrate VAL-81 VAL-84 VAL-82
|
|
224
|
+
mintree orchestrate VAL-81 VAL-84 -m auto
|
|
202
225
|
```
|
|
203
226
|
|
|
204
227
|
`mt`, `mtw`, `mtn` are shell aliases the wrapper installs for `mintree`, `mintree worktree`, and an interactive "name a branch" shortcut.
|
|
@@ -12,9 +12,10 @@ import { getLatestVersion, isNewerVersion } from "../lib/version.js";
|
|
|
12
12
|
import { ALLOWED_TYPES } from "../lib/branch.js";
|
|
13
13
|
import { runCreate, runCreateDetached, } from "../lib/worktreeCreate.js";
|
|
14
14
|
import { runRemove, runRemoveByPath } from "../lib/worktreeRemove.js";
|
|
15
|
-
import {
|
|
15
|
+
import { writePromptFile } from "../lib/worktreeCreate.js";
|
|
16
|
+
import { buildCreateMarkers, buildOrchestrateMarkers, emitMarkers } from "../lib/markers.js";
|
|
16
17
|
import { readMetadata } from "../lib/metadata.js";
|
|
17
|
-
import { renderPromptTemplate } from "../lib/promptTemplate.js";
|
|
18
|
+
import { defaultOrchestratorPrompt, renderOrchestratorTemplate, renderPromptTemplate, } from "../lib/promptTemplate.js";
|
|
18
19
|
import { createProvider } from "../lib/providers/index.js";
|
|
19
20
|
import { loadDashboard } from "../lib/dashboard.js";
|
|
20
21
|
import { priorityDisplay } from "../lib/priority.js";
|
|
@@ -33,15 +34,27 @@ function issueMatchesFilter(d, filter) {
|
|
|
33
34
|
return d.issue.id.replace(/\D/g, "").includes(filter);
|
|
34
35
|
}
|
|
35
36
|
function tabIssues(issues, tab, filter = "") {
|
|
36
|
-
|
|
37
|
+
// Orchestrate shows the same set as Issues (open issues assigned to you,
|
|
38
|
+
// non-orphan); only Worktrees flips to the orphan set.
|
|
39
|
+
return issues.filter((d) => (tab === "worktrees" ? isOrphan(d) : !isOrphan(d)) && issueMatchesFilter(d, filter));
|
|
40
|
+
}
|
|
41
|
+
function tabIndex(s, tab) {
|
|
42
|
+
if (tab === "issues")
|
|
43
|
+
return s.issuesIndex;
|
|
44
|
+
if (tab === "worktrees")
|
|
45
|
+
return s.worktreesIndex;
|
|
46
|
+
return s.orchestrateIndex;
|
|
37
47
|
}
|
|
38
48
|
function currentSelected(s) {
|
|
39
49
|
const displayed = tabIssues(s.issues, s.activeTab, s.filter);
|
|
40
|
-
|
|
41
|
-
return { displayed, selectedIndex };
|
|
50
|
+
return { displayed, selectedIndex: tabIndex(s, s.activeTab) };
|
|
42
51
|
}
|
|
43
52
|
function withSelectedIndex(s, next) {
|
|
44
|
-
|
|
53
|
+
if (s.activeTab === "issues")
|
|
54
|
+
return { ...s, issuesIndex: next };
|
|
55
|
+
if (s.activeTab === "worktrees")
|
|
56
|
+
return { ...s, worktreesIndex: next };
|
|
57
|
+
return { ...s, orchestrateIndex: next };
|
|
45
58
|
}
|
|
46
59
|
// xterm/iTerm/etc switch to the alternate screen buffer with these escape
|
|
47
60
|
// codes. Using the buffer means the dashboard owns the whole window for its
|
|
@@ -183,12 +196,18 @@ function useTerminalSize() {
|
|
|
183
196
|
}, [stdout]);
|
|
184
197
|
return size;
|
|
185
198
|
}
|
|
186
|
-
|
|
199
|
+
// Tab order for ← / → cycling.
|
|
200
|
+
const TAB_ORDER = ["issues", "worktrees", "orchestrate"];
|
|
201
|
+
function TabChip({ label, active }) {
|
|
202
|
+
return active ? (_jsx(Text, { bold: true, backgroundColor: "cyan", color: "black", children: label })) : (_jsx(Text, { dimColor: true, children: label }));
|
|
203
|
+
}
|
|
204
|
+
function HeaderRow({ repoName, claudeVersion, issueCount, worktreeCount, orchestrateCount, activeTab, updateAvailable, }) {
|
|
187
205
|
const issuesLabel = ` Issues (${issueCount}) `;
|
|
188
206
|
const worktreesLabel = ` Worktrees (${worktreeCount}) `;
|
|
189
|
-
|
|
207
|
+
const orchestrateLabel = orchestrateCount > 0 ? ` Orchestrate (${orchestrateCount}) ` : ` Orchestrate `;
|
|
208
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "green", children: "mintree" }), _jsx(Text, { dimColor: true, children: ` v${mintreeVersion}` }), updateAvailable && _jsx(Text, { color: "yellow", children: " (*)" }), claudeVersion && _jsx(Text, { dimColor: true, children: ` · claude ${claudeVersion}` }), repoName && _jsx(Text, { dimColor: true, children: ` · ${repoName}` })] }), _jsxs(Box, { children: [_jsx(TabChip, { label: issuesLabel, active: activeTab === "issues" }), _jsx(Text, { children: " " }), _jsx(TabChip, { label: worktreesLabel, active: activeTab === "worktrees" }), _jsx(Text, { children: " " }), _jsx(TabChip, { label: orchestrateLabel, active: activeTab === "orchestrate" }), _jsx(Text, { dimColor: true, children: " ← / → switch tab" })] })] }));
|
|
190
209
|
}
|
|
191
|
-
function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
210
|
+
function FooterRow({ phase, overlayKind, latestVersion, listWidth, activeTab, selectedCount, }) {
|
|
192
211
|
if (phase === "error") {
|
|
193
212
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "q quit" }) }));
|
|
194
213
|
}
|
|
@@ -198,6 +217,11 @@ function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
|
198
217
|
if (overlayKind === "remove") {
|
|
199
218
|
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "y/Y" }), _jsx(Text, { dimColor: true, children: " confirm " }), _jsx(Text, { bold: true, children: "n/Esc" }), _jsx(Text, { dimColor: true, children: " cancel" })] }));
|
|
200
219
|
}
|
|
220
|
+
// Orchestrate tab: selection-driven controls instead of the per-ticket
|
|
221
|
+
// work/open/remove actions.
|
|
222
|
+
if (activeTab === "orchestrate") {
|
|
223
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { 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: " Space" }), _jsx(Text, { dimColor: true, children: " toggle " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " a" }), _jsx(Text, { dimColor: true, children: " all " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " \u21B5" }), _jsxs(Text, { dimColor: true, children: [" orchestrate", selectedCount ? ` (${selectedCount})` : "", " "] }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " q" }), _jsx(Text, { dimColor: true, children: " quit" })] }), latestVersion && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "(*)" }), _jsx(Text, { dimColor: true, children: ` new version available — v${latestVersion} · npm i -g mintree` })] }))] }));
|
|
224
|
+
}
|
|
201
225
|
// Two-column footer like santree: common navigation/dashboard commands
|
|
202
226
|
// align under the left (list) pane; ticket-specific actions align under
|
|
203
227
|
// the right (detail) pane. Falls back to a single inline row when no
|
|
@@ -236,7 +260,7 @@ function CreateStepIcon({ kind }) {
|
|
|
236
260
|
return _jsx(Text, { color: "yellow", children: "!" });
|
|
237
261
|
return _jsx(Text, { color: "cyan", children: "\u25CB" });
|
|
238
262
|
}
|
|
239
|
-
function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
263
|
+
function IssueListRow({ d, selected, identifierWidth, rowWidth, checkbox, }) {
|
|
240
264
|
// Display the issue id raw (e.g. "FE-123", "100"). The `#` prefix is a
|
|
241
265
|
// GitHub convention that reads as noise for Linear's already-prefixed
|
|
242
266
|
// ids, and dropping it across the board keeps the dashboard provider-
|
|
@@ -250,12 +274,14 @@ function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
|
250
274
|
// priority. See lib/priority.ts.
|
|
251
275
|
const prio = priorityDisplay(d.issue.priority);
|
|
252
276
|
const title = d.issue.title;
|
|
277
|
+
const checkPrefix = checkbox === undefined ? " " : checkbox === "on" ? "[✔] " : "[ ] ";
|
|
278
|
+
const checkColor = checkbox === "on" ? "green" : undefined;
|
|
253
279
|
// The leading-dot Text and the rest are nested under a single Text so the
|
|
254
280
|
// selection background paints the whole row in one contiguous block.
|
|
255
281
|
// `wrap="truncate"` clamps the row to a single line and Ink renders an
|
|
256
282
|
// ellipsis at the cut. The outer Box has a fixed width so the wrap
|
|
257
283
|
// behaviour knows where to truncate.
|
|
258
|
-
return (_jsx(Box, { width: rowWidth, children: _jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "blue" : undefined, color: selected ? "white" : undefined, children: ["
|
|
284
|
+
return (_jsx(Box, { width: rowWidth, children: _jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "blue" : undefined, color: selected ? "white" : undefined, children: [_jsx(Text, { color: selected ? "white" : checkColor, children: checkPrefix }), _jsx(Text, { color: selected ? "white" : dotColor, children: "\u25CF" }), " ", _jsx(Text, { color: selected ? "white" : prio.color, children: prio.icon }), ` ${idText} ${title}`] }) }));
|
|
259
285
|
}
|
|
260
286
|
// A project board header — the top level of the grouped issue list. Mirrors
|
|
261
287
|
// the bold project name + dim count seen in the santree dashboard.
|
|
@@ -406,7 +432,7 @@ function windowListRows(listRows, selectedIndex, viewportRows) {
|
|
|
406
432
|
}
|
|
407
433
|
// Renders a single grouped-list row — used for both the sticky header region
|
|
408
434
|
// and the scrollable body so the two stay visually identical.
|
|
409
|
-
function ListRowView({ row, selectedIndex, identifierWidth, width, }) {
|
|
435
|
+
function ListRowView({ row, selectedIndex, identifierWidth, width, selectedIds, }) {
|
|
410
436
|
if (row.kind === "spacer")
|
|
411
437
|
return _jsx(Text, { children: " " });
|
|
412
438
|
if (row.kind === "project") {
|
|
@@ -415,7 +441,7 @@ function ListRowView({ row, selectedIndex, identifierWidth, width, }) {
|
|
|
415
441
|
if (row.kind === "status") {
|
|
416
442
|
return _jsx(StatusHeaderRow, { name: row.name, color: row.color, count: row.count, width: width });
|
|
417
443
|
}
|
|
418
|
-
return (_jsx(IssueListRow, { d: row.d, selected: row.index === selectedIndex, identifierWidth: identifierWidth, rowWidth: width }));
|
|
444
|
+
return (_jsx(IssueListRow, { d: row.d, selected: row.index === selectedIndex, identifierWidth: identifierWidth, rowWidth: width, checkbox: selectedIds ? (selectedIds.has(row.d.issue.id) ? "on" : "off") : undefined }));
|
|
419
445
|
}
|
|
420
446
|
// Word-wraps a single line at `width` columns, breaking on the last space
|
|
421
447
|
// before the limit when that yields a reasonable cut. Falls back to a hard
|
|
@@ -695,22 +721,42 @@ export default function Dashboard() {
|
|
|
695
721
|
const activeTab = prevReady?.activeTab ?? "issues";
|
|
696
722
|
const previousIssuesIndex = prevReady?.issuesIndex ?? 0;
|
|
697
723
|
const previousWorktreesIndex = prevReady?.worktreesIndex ?? 0;
|
|
724
|
+
const previousOrchestrateIndex = prevReady?.orchestrateIndex ?? 0;
|
|
698
725
|
const previousOverlay = prevReady?.overlay ?? null;
|
|
699
726
|
const previousToast = prevReady?.toast ?? null;
|
|
700
727
|
const previousScroll = prevReady?.detailScrollOffset ?? 0;
|
|
701
728
|
const filter = prevReady?.filter ?? "";
|
|
702
729
|
const issuesList = tabIssues(issues, "issues", filter);
|
|
703
730
|
const worktreesList = tabIssues(issues, "worktrees", filter);
|
|
731
|
+
const orchestrateList = tabIssues(issues, "orchestrate", filter);
|
|
704
732
|
const issuesIndex = Math.min(previousIssuesIndex, Math.max(0, issuesList.length - 1));
|
|
705
733
|
const worktreesIndex = Math.min(previousWorktreesIndex, Math.max(0, worktreesList.length - 1));
|
|
734
|
+
const orchestrateIndex = Math.min(previousOrchestrateIndex, Math.max(0, orchestrateList.length - 1));
|
|
735
|
+
// Keep only checked ids that still exist among the open issues, so a
|
|
736
|
+
// resolved/closed ticket drops out of the batch instead of lingering.
|
|
737
|
+
const liveIds = new Set(issues.map((d) => d.issue.id));
|
|
738
|
+
const selectedIds = new Set([...(prevReady?.selectedIds ?? [])].filter((id) => liveIds.has(id)));
|
|
706
739
|
// Preserve scroll only when the active tab's selected issue still
|
|
707
740
|
// resolves to the same row — clamping or list churn means the user
|
|
708
741
|
// is now reading something else.
|
|
709
742
|
const prevDisplayed = prevReady ? tabIssues(prevReady.issues, activeTab, filter) : [];
|
|
710
|
-
const nextDisplayed = activeTab === "
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
743
|
+
const nextDisplayed = activeTab === "worktrees"
|
|
744
|
+
? worktreesList
|
|
745
|
+
: activeTab === "orchestrate"
|
|
746
|
+
? orchestrateList
|
|
747
|
+
: issuesList;
|
|
748
|
+
const prevIdx = activeTab === "worktrees"
|
|
749
|
+
? previousWorktreesIndex
|
|
750
|
+
: activeTab === "orchestrate"
|
|
751
|
+
? previousOrchestrateIndex
|
|
752
|
+
: previousIssuesIndex;
|
|
753
|
+
const nextIdx = activeTab === "worktrees"
|
|
754
|
+
? worktreesIndex
|
|
755
|
+
: activeTab === "orchestrate"
|
|
756
|
+
? orchestrateIndex
|
|
757
|
+
: issuesIndex;
|
|
758
|
+
const prevSelectedId = prevDisplayed[prevIdx]?.issue.id ?? null;
|
|
759
|
+
const nextSelectedId = nextDisplayed[nextIdx]?.issue.id ?? null;
|
|
714
760
|
const detailScrollOffset = prevSelectedId !== null && prevSelectedId === nextSelectedId ? previousScroll : 0;
|
|
715
761
|
return {
|
|
716
762
|
phase: "ready",
|
|
@@ -718,6 +764,8 @@ export default function Dashboard() {
|
|
|
718
764
|
activeTab,
|
|
719
765
|
issuesIndex,
|
|
720
766
|
worktreesIndex,
|
|
767
|
+
orchestrateIndex,
|
|
768
|
+
selectedIds,
|
|
721
769
|
detailScrollOffset,
|
|
722
770
|
refreshing: false,
|
|
723
771
|
overlay: previousOverlay,
|
|
@@ -847,7 +895,14 @@ export default function Dashboard() {
|
|
|
847
895
|
// Esc clears an active numeric filter before it falls through to quit —
|
|
848
896
|
// so the user can back out of a search without leaving the dashboard.
|
|
849
897
|
if (key.escape && state.phase === "ready" && state.filter) {
|
|
850
|
-
setState({
|
|
898
|
+
setState({
|
|
899
|
+
...state,
|
|
900
|
+
filter: "",
|
|
901
|
+
issuesIndex: 0,
|
|
902
|
+
worktreesIndex: 0,
|
|
903
|
+
orchestrateIndex: 0,
|
|
904
|
+
detailScrollOffset: 0,
|
|
905
|
+
});
|
|
851
906
|
return;
|
|
852
907
|
}
|
|
853
908
|
if (input === "q" || key.escape || (input === "c" && key.ctrl)) {
|
|
@@ -866,6 +921,7 @@ export default function Dashboard() {
|
|
|
866
921
|
filter: state.filter + input,
|
|
867
922
|
issuesIndex: 0,
|
|
868
923
|
worktreesIndex: 0,
|
|
924
|
+
orchestrateIndex: 0,
|
|
869
925
|
detailScrollOffset: 0,
|
|
870
926
|
});
|
|
871
927
|
return;
|
|
@@ -876,14 +932,17 @@ export default function Dashboard() {
|
|
|
876
932
|
filter: state.filter.slice(0, -1),
|
|
877
933
|
issuesIndex: 0,
|
|
878
934
|
worktreesIndex: 0,
|
|
935
|
+
orchestrateIndex: 0,
|
|
879
936
|
detailScrollOffset: 0,
|
|
880
937
|
});
|
|
881
938
|
return;
|
|
882
939
|
}
|
|
883
940
|
if (key.leftArrow || key.rightArrow) {
|
|
884
|
-
//
|
|
885
|
-
// preserved, so the user returns to the row they left.
|
|
886
|
-
const
|
|
941
|
+
// Cycle through the three tabs; → advances, ← goes back. Per-tab
|
|
942
|
+
// indices are preserved, so the user returns to the row they left.
|
|
943
|
+
const cur = TAB_ORDER.indexOf(state.activeTab);
|
|
944
|
+
const delta = key.leftArrow ? -1 : 1;
|
|
945
|
+
const next = TAB_ORDER[(cur + delta + TAB_ORDER.length) % TAB_ORDER.length];
|
|
887
946
|
setState({ ...state, activeTab: next, detailScrollOffset: 0 });
|
|
888
947
|
return;
|
|
889
948
|
}
|
|
@@ -918,6 +977,34 @@ export default function Dashboard() {
|
|
|
918
977
|
void refresh();
|
|
919
978
|
return;
|
|
920
979
|
}
|
|
980
|
+
// Orchestrate tab: Space toggles the ticket under the cursor; `a`
|
|
981
|
+
// toggles all visible tickets at once.
|
|
982
|
+
if (state.activeTab === "orchestrate" && input === " ") {
|
|
983
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
984
|
+
const issue = displayed[selectedIndex];
|
|
985
|
+
if (!issue)
|
|
986
|
+
return;
|
|
987
|
+
const next = new Set(state.selectedIds);
|
|
988
|
+
if (next.has(issue.issue.id))
|
|
989
|
+
next.delete(issue.issue.id);
|
|
990
|
+
else
|
|
991
|
+
next.add(issue.issue.id);
|
|
992
|
+
setState({ ...state, selectedIds: next });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (state.activeTab === "orchestrate" && input === "a") {
|
|
996
|
+
const { displayed } = currentSelected(state);
|
|
997
|
+
const allSelected = displayed.length > 0 && displayed.every((d) => state.selectedIds.has(d.issue.id));
|
|
998
|
+
const next = new Set(state.selectedIds);
|
|
999
|
+
for (const d of displayed) {
|
|
1000
|
+
if (allSelected)
|
|
1001
|
+
next.delete(d.issue.id);
|
|
1002
|
+
else
|
|
1003
|
+
next.add(d.issue.id);
|
|
1004
|
+
}
|
|
1005
|
+
setState({ ...state, selectedIds: next });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
921
1008
|
if (input === "o") {
|
|
922
1009
|
const { displayed, selectedIndex } = currentSelected(state);
|
|
923
1010
|
const issue = displayed[selectedIndex];
|
|
@@ -940,6 +1027,12 @@ export default function Dashboard() {
|
|
|
940
1027
|
return;
|
|
941
1028
|
}
|
|
942
1029
|
if (key.return) {
|
|
1030
|
+
// Orchestrate tab: Enter launches the orchestrator over the checked
|
|
1031
|
+
// tickets instead of resuming/creating a single worktree.
|
|
1032
|
+
if (state.activeTab === "orchestrate") {
|
|
1033
|
+
launchOrchestrator();
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
943
1036
|
const { displayed, selectedIndex } = currentSelected(state);
|
|
944
1037
|
const issue = displayed[selectedIndex];
|
|
945
1038
|
if (!issue)
|
|
@@ -976,6 +1069,41 @@ export default function Dashboard() {
|
|
|
976
1069
|
return;
|
|
977
1070
|
}
|
|
978
1071
|
});
|
|
1072
|
+
function launchOrchestrator() {
|
|
1073
|
+
if (state.phase !== "ready")
|
|
1074
|
+
return;
|
|
1075
|
+
const { displayed } = currentSelected(state);
|
|
1076
|
+
// Preserve the display order; only keep ids that are actually visible
|
|
1077
|
+
// and checked.
|
|
1078
|
+
const ids = displayed.filter((d) => state.selectedIds.has(d.issue.id)).map((d) => d.issue.id);
|
|
1079
|
+
if (ids.length === 0) {
|
|
1080
|
+
setState({
|
|
1081
|
+
...state,
|
|
1082
|
+
toast: {
|
|
1083
|
+
kind: "error",
|
|
1084
|
+
text: "Seleccioná al menos un ticket (Space) antes de orquestar.",
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const root = findMainRepoRoot();
|
|
1090
|
+
if (!root) {
|
|
1091
|
+
setState({ ...state, toast: { kind: "error", text: "No estás en un repositorio git." } });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const meta = readMetadata(root);
|
|
1095
|
+
const idList = ids.join(", ");
|
|
1096
|
+
const prompt = meta.orchestratorPromptTemplate
|
|
1097
|
+
? renderOrchestratorTemplate(meta.orchestratorPromptTemplate, {
|
|
1098
|
+
ids: idList,
|
|
1099
|
+
count: ids.length,
|
|
1100
|
+
})
|
|
1101
|
+
: defaultOrchestratorPrompt(idList);
|
|
1102
|
+
const promptFile = writePromptFile(prompt);
|
|
1103
|
+
const permissionMode = meta.defaultPermissionMode ?? "default";
|
|
1104
|
+
emitMarkers(buildOrchestrateMarkers({ repoRoot: root, promptFile, permissionMode }));
|
|
1105
|
+
exit();
|
|
1106
|
+
}
|
|
979
1107
|
function openCreateOverlay(issue) {
|
|
980
1108
|
if (state.phase !== "ready")
|
|
981
1109
|
return;
|
|
@@ -1240,11 +1368,13 @@ export default function Dashboard() {
|
|
|
1240
1368
|
if (state.phase === "error") {
|
|
1241
1369
|
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" }) })] }));
|
|
1242
1370
|
}
|
|
1243
|
-
const { issues, refreshing, overlay, toast, activeTab, filter } = state;
|
|
1371
|
+
const { issues, refreshing, overlay, toast, activeTab, filter, selectedIds } = state;
|
|
1244
1372
|
const { displayed, selectedIndex } = currentSelected(state);
|
|
1245
1373
|
const selected = displayed[selectedIndex] ?? null;
|
|
1246
1374
|
const issuesTabCount = issues.reduce((n, d) => (isOrphan(d) ? n : n + 1), 0);
|
|
1247
1375
|
const worktreesTabCount = issues.length - issuesTabCount;
|
|
1376
|
+
// The Orchestrate chip shows how many tickets are currently checked.
|
|
1377
|
+
const orchestrateTabCount = selectedIds.size;
|
|
1248
1378
|
const onOverlayDescChange = (next) => {
|
|
1249
1379
|
if (state.phase !== "ready" || !state.overlay)
|
|
1250
1380
|
return;
|
|
@@ -1292,9 +1422,9 @@ export default function Dashboard() {
|
|
|
1292
1422
|
: displayed.map((d, index) => ({ kind: "issue", d, index }));
|
|
1293
1423
|
const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
|
|
1294
1424
|
const listContentWidth = Math.max(8, listWidth - 4);
|
|
1295
|
-
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
|
|
1425
|
+
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, orchestrateCount: orchestrateTabCount, 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
|
|
1296
1426
|
? `No tickets match #${filter} — Esc to clear the filter.`
|
|
1297
|
-
: activeTab === "
|
|
1298
|
-
? "No
|
|
1299
|
-
: "No
|
|
1427
|
+
: activeTab === "worktrees"
|
|
1428
|
+
? "No orphaned worktrees — anything in `.mintree/worktrees/` matches an open issue."
|
|
1429
|
+
: "No open issues assigned to you in this repo." })) : (_jsxs(_Fragment, { children: [listView.sticky.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth, selectedIds: activeTab === "orchestrate" ? selectedIds : undefined }, `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, selectedIds: activeTab === "orchestrate" ? selectedIds : undefined }, `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, activeTab: activeTab, selectedCount: orchestrateTabCount })] })] }));
|
|
1300
1430
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "Launch a Claude orchestrator in the repo root to resolve a batch of tickets";
|
|
3
|
+
export declare const args: z.ZodArray<z.ZodString>;
|
|
4
|
+
export declare const options: z.ZodObject<{
|
|
5
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
6
|
+
promptFile: z.ZodOptional<z.ZodString>;
|
|
7
|
+
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
8
|
+
default: "default";
|
|
9
|
+
auto: "auto";
|
|
10
|
+
}>>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
type Props = {
|
|
13
|
+
args: z.infer<typeof args>;
|
|
14
|
+
options: z.infer<typeof options>;
|
|
15
|
+
};
|
|
16
|
+
export default function Orchestrate({ args: ids, options }: Props): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { argument, option } from "pastel";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
import { readFileSync, unlinkSync } from "fs";
|
|
9
|
+
import { findMainRepoRoot, getMintreeDir, pathExists } from "../lib/git.js";
|
|
10
|
+
import { readMetadata } from "../lib/metadata.js";
|
|
11
|
+
import { launchClaude, PERMISSION_MODES } from "../lib/claude.js";
|
|
12
|
+
import { defaultOrchestratorPrompt, renderOrchestratorTemplate } from "../lib/promptTemplate.js";
|
|
13
|
+
export const description = "Launch a Claude orchestrator in the repo root to resolve a batch of tickets";
|
|
14
|
+
export const args = z.array(z.string()).describe(argument({
|
|
15
|
+
name: "ids",
|
|
16
|
+
description: "Ticket ids to orchestrate (e.g. VAL-81 VAL-84). Renders the orchestratorPromptTemplate (or the built-in default). Ignored when --prompt / --prompt-file is given.",
|
|
17
|
+
}));
|
|
18
|
+
export const options = z.object({
|
|
19
|
+
prompt: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(option({
|
|
23
|
+
description: "Literal orchestrator message (overrides the template/ids).",
|
|
24
|
+
})),
|
|
25
|
+
promptFile: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(option({
|
|
29
|
+
description: "Read the orchestrator message from this file (deleted after read). Used by the dashboard's Orchestrate tab. Mutually exclusive with --prompt.",
|
|
30
|
+
})),
|
|
31
|
+
permissionMode: z
|
|
32
|
+
.enum(PERMISSION_MODES)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe(option({
|
|
35
|
+
description: `Claude --permission-mode (one of: ${PERMISSION_MODES.join(", ")}). Defaults to metadata.defaultPermissionMode, else "default".`,
|
|
36
|
+
alias: "m",
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
39
|
+
function resolve(cwd, ids, opts) {
|
|
40
|
+
if (opts.prompt && opts.promptFile) {
|
|
41
|
+
return { ok: false, message: "--prompt and --prompt-file are mutually exclusive." };
|
|
42
|
+
}
|
|
43
|
+
const repoRoot = findMainRepoRoot(cwd);
|
|
44
|
+
if (!repoRoot) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
message: "Not in a git repository.",
|
|
48
|
+
hint: "Run `mintree orchestrate` from inside a mintree-enabled repo.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (!pathExists(getMintreeDir(repoRoot))) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
message: ".mintree/ not found in this repo.",
|
|
55
|
+
hint: "Run `mintree init` first.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Resolve the orchestrator message. Priority: --prompt-file (the dashboard
|
|
59
|
+
// path) > --prompt (literal) > render the template from the ticket ids.
|
|
60
|
+
let prompt;
|
|
61
|
+
if (opts.promptFile) {
|
|
62
|
+
try {
|
|
63
|
+
prompt = readFileSync(opts.promptFile, "utf-8");
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
message: `Could not read --prompt-file ${opts.promptFile}.`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
unlinkSync(opts.promptFile);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Cleanup failure is non-fatal.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (opts.prompt) {
|
|
79
|
+
prompt = opts.prompt;
|
|
80
|
+
}
|
|
81
|
+
else if (ids.length > 0) {
|
|
82
|
+
const idList = ids.join(", ");
|
|
83
|
+
const template = readMetadata(repoRoot).orchestratorPromptTemplate;
|
|
84
|
+
prompt = template
|
|
85
|
+
? renderOrchestratorTemplate(template, { ids: idList, count: ids.length })
|
|
86
|
+
: defaultOrchestratorPrompt(idList);
|
|
87
|
+
}
|
|
88
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
message: "Nothing to orchestrate.",
|
|
92
|
+
hint: "Pass ticket ids (e.g. `mintree orchestrate VAL-81 VAL-84`) or a --prompt.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const permissionMode = opts.permissionMode ?? readMetadata(repoRoot).defaultPermissionMode ?? "default";
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
data: { repoRoot, sessionId: randomUUID(), permissionMode, prompt },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export default function Orchestrate({ args: ids, options }) {
|
|
102
|
+
const [state, setState] = useState({ phase: "loading" });
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
const result = resolve(process.cwd(), ids, options);
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
setState({ phase: "error", message: result.message, hint: result.hint });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setState({ phase: "launching", resolved: result.data });
|
|
111
|
+
}, 0);
|
|
112
|
+
}, []);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (state.phase !== "launching")
|
|
115
|
+
return;
|
|
116
|
+
const { resolved } = state;
|
|
117
|
+
try {
|
|
118
|
+
const child = launchClaude({
|
|
119
|
+
permissionMode: resolved.permissionMode,
|
|
120
|
+
sessionId: resolved.sessionId,
|
|
121
|
+
resume: false,
|
|
122
|
+
prompt: resolved.prompt,
|
|
123
|
+
cwd: resolved.repoRoot,
|
|
124
|
+
remoteControlName: "orchestrator",
|
|
125
|
+
});
|
|
126
|
+
child.on("error", (err) => {
|
|
127
|
+
setState({ phase: "error", message: `Failed to launch claude: ${err.message}` });
|
|
128
|
+
});
|
|
129
|
+
child.on("close", (code) => {
|
|
130
|
+
process.exit(code ?? 0);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
setState({
|
|
135
|
+
phase: "error",
|
|
136
|
+
message: err instanceof Error ? err.message : String(err),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}, [state.phase]);
|
|
140
|
+
if (state.phase === "loading") {
|
|
141
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Resolving repo..." })] }));
|
|
142
|
+
}
|
|
143
|
+
if (state.phase === "error") {
|
|
144
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 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] }) }))] }));
|
|
145
|
+
}
|
|
146
|
+
const { resolved } = state;
|
|
147
|
+
const sessionShort = resolved.sessionId.slice(0, 8);
|
|
148
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree orchestrate" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", resolved.repoRoot] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "session: " }), _jsxs(Text, { children: [sessionShort, "\u2026"] }), _jsx(Text, { dimColor: true, children: " (starting)" })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "permission-mode: " }), _jsx(Text, { children: resolved.permissionMode })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "prompt: " }), _jsxs(Text, { children: ["\"", truncate(resolved.prompt.replace(/\n/g, " "), 60), "\""] })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude orchestrator..." }) })] }));
|
|
149
|
+
}
|
|
150
|
+
function truncate(s, max) {
|
|
151
|
+
if (s.length <= max)
|
|
152
|
+
return s;
|
|
153
|
+
return s.slice(0, max - 1) + "…";
|
|
154
|
+
}
|
package/dist/lib/markers.d.ts
CHANGED
|
@@ -19,3 +19,14 @@ export type CreateMarkers = {
|
|
|
19
19
|
* three work-related markers only when --work was on.
|
|
20
20
|
*/
|
|
21
21
|
export declare function buildCreateMarkers(input: CreateMarkers): string[];
|
|
22
|
+
export type OrchestrateMarkers = {
|
|
23
|
+
repoRoot: string;
|
|
24
|
+
promptFile: string;
|
|
25
|
+
permissionMode?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Builds the marker block emitted when the dashboard launches the orchestrator
|
|
29
|
+
* from the Orchestrate tab. The shell wrapper cd's to `repoRoot` and then runs
|
|
30
|
+
* `mintree orchestrate --prompt-file <file> [--permission-mode <mode>]`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildOrchestrateMarkers(input: OrchestrateMarkers): string[];
|
package/dist/lib/markers.js
CHANGED
|
@@ -41,3 +41,19 @@ export function buildCreateMarkers(input) {
|
|
|
41
41
|
}
|
|
42
42
|
return lines;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Builds the marker block emitted when the dashboard launches the orchestrator
|
|
46
|
+
* from the Orchestrate tab. The shell wrapper cd's to `repoRoot` and then runs
|
|
47
|
+
* `mintree orchestrate --prompt-file <file> [--permission-mode <mode>]`.
|
|
48
|
+
*/
|
|
49
|
+
export function buildOrchestrateMarkers(input) {
|
|
50
|
+
const lines = [
|
|
51
|
+
`MINTREE_CD:${input.repoRoot}`,
|
|
52
|
+
"MINTREE_ORCHESTRATE:1",
|
|
53
|
+
`MINTREE_ORCHESTRATE_PROMPT_FILE:${input.promptFile}`,
|
|
54
|
+
];
|
|
55
|
+
if (input.permissionMode) {
|
|
56
|
+
lines.push(`MINTREE_PERMISSION_MODE:${input.permissionMode}`);
|
|
57
|
+
}
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
package/dist/lib/metadata.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export type Metadata = {
|
|
|
29
29
|
linear?: LinearMeta;
|
|
30
30
|
defaultPermissionMode?: PermissionMode;
|
|
31
31
|
promptTemplate?: string;
|
|
32
|
+
orchestratorPromptTemplate?: string;
|
|
32
33
|
};
|
|
33
34
|
export declare function readMetadata(repoRoot: string): Metadata;
|
|
34
35
|
export declare function writeMetadata(repoRoot: string, data: Metadata): void;
|
package/dist/lib/metadata.js
CHANGED
|
@@ -13,6 +13,9 @@ function sanitizePermissionMode(raw) {
|
|
|
13
13
|
function sanitizePromptTemplate(raw) {
|
|
14
14
|
return typeof raw === "string" && raw.trim().length > 0 ? raw : undefined;
|
|
15
15
|
}
|
|
16
|
+
function sanitizeOrchestratorPromptTemplate(raw) {
|
|
17
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw : undefined;
|
|
18
|
+
}
|
|
16
19
|
function sanitizeLinearTeam(raw) {
|
|
17
20
|
if (typeof raw !== "object" || raw === null)
|
|
18
21
|
return undefined;
|
|
@@ -86,6 +89,7 @@ export function readMetadata(repoRoot) {
|
|
|
86
89
|
const linear = sanitizeLinear(parsed.linear);
|
|
87
90
|
const defaultPermissionMode = sanitizePermissionMode(parsed.defaultPermissionMode);
|
|
88
91
|
const promptTemplate = sanitizePromptTemplate(parsed.promptTemplate);
|
|
92
|
+
const orchestratorPromptTemplate = sanitizeOrchestratorPromptTemplate(parsed.orchestratorPromptTemplate);
|
|
89
93
|
return {
|
|
90
94
|
version: 1,
|
|
91
95
|
issues: typeof parsed.issues === "object" && parsed.issues !== null
|
|
@@ -96,6 +100,7 @@ export function readMetadata(repoRoot) {
|
|
|
96
100
|
...(linear ? { linear } : {}),
|
|
97
101
|
...(defaultPermissionMode ? { defaultPermissionMode } : {}),
|
|
98
102
|
...(promptTemplate ? { promptTemplate } : {}),
|
|
103
|
+
...(orchestratorPromptTemplate ? { orchestratorPromptTemplate } : {}),
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
106
|
catch {
|
|
@@ -16,3 +16,30 @@ export declare const PROMPT_PLACEHOLDERS: readonly ["{{id}}", "{{title}}", "{{ur
|
|
|
16
16
|
* is visible in the launched prompt instead of silently vanishing.
|
|
17
17
|
*/
|
|
18
18
|
export declare function renderPromptTemplate(template: string, vars: PromptVars): string;
|
|
19
|
+
/**
|
|
20
|
+
* Variables available to an `orchestratorPromptTemplate`. The orchestrator
|
|
21
|
+
* works a batch of tickets, so it only needs the list of ids and the count —
|
|
22
|
+
* not per-issue title/url (the orchestrator looks those up itself).
|
|
23
|
+
*/
|
|
24
|
+
export type OrchestratorVars = {
|
|
25
|
+
ids: string;
|
|
26
|
+
count: number;
|
|
27
|
+
};
|
|
28
|
+
export declare const ORCHESTRATOR_PLACEHOLDERS: readonly ["{{ids}}", "{{count}}"];
|
|
29
|
+
/**
|
|
30
|
+
* Renders an `orchestratorPromptTemplate` by substituting the `{{ids}}` and
|
|
31
|
+
* `{{count}}` placeholders. Whitespace inside the braces is tolerated
|
|
32
|
+
* (`{{ ids }}`); unknown placeholders are left untouched so a typo is visible
|
|
33
|
+
* in the launched prompt instead of silently vanishing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderOrchestratorTemplate(template: string, vars: OrchestratorVars): string;
|
|
36
|
+
/**
|
|
37
|
+
* Built-in default for the orchestrator message, used when the repo doesn't
|
|
38
|
+
* configure an `orchestratorPromptTemplate`. Mirrors the manual flow the user
|
|
39
|
+
* was running by hand: act as an orchestrator over the selected tickets,
|
|
40
|
+
* resolve them with minimal intervention, parallelise via subagents unless
|
|
41
|
+
* dependencies force sequential work, and for each ticket follow the repo
|
|
42
|
+
* conventions, create the worktree with mintree, use the right skills, move it
|
|
43
|
+
* to "in progress" on start and close it when done.
|
|
44
|
+
*/
|
|
45
|
+
export declare function defaultOrchestratorPrompt(ids: string): string;
|
|
@@ -13,3 +13,38 @@ export function renderPromptTemplate(template, vars) {
|
|
|
13
13
|
.replace(/\{\{\s*title\s*\}\}/g, vars.title)
|
|
14
14
|
.replace(/\{\{\s*url\s*\}\}/g, vars.url);
|
|
15
15
|
}
|
|
16
|
+
// Placeholder tokens for an `orchestratorPromptTemplate`. Kept in sync with
|
|
17
|
+
// the README and any `init`/help output.
|
|
18
|
+
export const ORCHESTRATOR_PLACEHOLDERS = ["{{ids}}", "{{count}}"];
|
|
19
|
+
/**
|
|
20
|
+
* Renders an `orchestratorPromptTemplate` by substituting the `{{ids}}` and
|
|
21
|
+
* `{{count}}` placeholders. Whitespace inside the braces is tolerated
|
|
22
|
+
* (`{{ ids }}`); unknown placeholders are left untouched so a typo is visible
|
|
23
|
+
* in the launched prompt instead of silently vanishing.
|
|
24
|
+
*/
|
|
25
|
+
export function renderOrchestratorTemplate(template, vars) {
|
|
26
|
+
return template
|
|
27
|
+
.replace(/\{\{\s*ids\s*\}\}/g, vars.ids)
|
|
28
|
+
.replace(/\{\{\s*count\s*\}\}/g, String(vars.count));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Built-in default for the orchestrator message, used when the repo doesn't
|
|
32
|
+
* configure an `orchestratorPromptTemplate`. Mirrors the manual flow the user
|
|
33
|
+
* was running by hand: act as an orchestrator over the selected tickets,
|
|
34
|
+
* resolve them with minimal intervention, parallelise via subagents unless
|
|
35
|
+
* dependencies force sequential work, and for each ticket follow the repo
|
|
36
|
+
* conventions, create the worktree with mintree, use the right skills, move it
|
|
37
|
+
* to "in progress" on start and close it when done.
|
|
38
|
+
*/
|
|
39
|
+
export function defaultOrchestratorPrompt(ids) {
|
|
40
|
+
return [
|
|
41
|
+
`Quiero que hagas de orquestador con los tickets ${ids}.`,
|
|
42
|
+
"",
|
|
43
|
+
"La idea es que resuelvas esos tickets con la menor intervención mía posible.",
|
|
44
|
+
"Trabajá los tickets en paralelo creando subagentes (a no ser que tengan",
|
|
45
|
+
"dependencias entre sí y no se puedan paralelizar, en cuyo caso trabajalos",
|
|
46
|
+
"secuencialmente). Para cada ticket: seguí los lineamientos del repo, creá el",
|
|
47
|
+
"worktree usando mintree, usá las skills correctas para cada caso, poné el ticket",
|
|
48
|
+
'en "in progress" al empezar y cerralo al terminar.',
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
@@ -39,6 +39,12 @@ export type CreateResult = {
|
|
|
39
39
|
message: string;
|
|
40
40
|
hint?: string;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Stashes a `--prompt` value into a temp file so the shell wrapper can hand
|
|
44
|
+
* it back to `worktree work` via `--prompt-file`. Plain stdout markers can't
|
|
45
|
+
* carry multi-line / shell-special text safely, hence the file.
|
|
46
|
+
*/
|
|
47
|
+
export declare function writePromptFile(prompt: string): string;
|
|
42
48
|
/**
|
|
43
49
|
* The whole `worktree create` flow as a pure function — same code path used
|
|
44
50
|
* by the CLI command and by the dashboard's `w` overlay. Validates input,
|
|
@@ -54,7 +54,7 @@ function tryRunInitScript(scriptPath, worktreePath, repoRoot) {
|
|
|
54
54
|
* it back to `worktree work` via `--prompt-file`. Plain stdout markers can't
|
|
55
55
|
* carry multi-line / shell-special text safely, hence the file.
|
|
56
56
|
*/
|
|
57
|
-
function writePromptFile(prompt) {
|
|
57
|
+
export function writePromptFile(prompt) {
|
|
58
58
|
const fileName = `mintree-prompt-${process.pid}-${Date.now()}.txt`;
|
|
59
59
|
const filePath = path.join(os.tmpdir(), fileName);
|
|
60
60
|
fs.writeFileSync(filePath, prompt);
|
package/package.json
CHANGED
package/shell/init.bash
CHANGED
|
@@ -43,14 +43,28 @@ function mintree() {
|
|
|
43
43
|
fi
|
|
44
44
|
cd "$target_dir" && echo "Switched to: $target_dir"
|
|
45
45
|
|
|
46
|
+
local perm_mode
|
|
47
|
+
perm_mode=$(echo "$clean_output" | grep "MINTREE_PERMISSION_MODE:" | sed 's/.*MINTREE_PERMISSION_MODE://')
|
|
48
|
+
|
|
49
|
+
# Orchestrate: launch a Claude orchestrator in the repo root (already
|
|
50
|
+
# cd'd above) with the batch prompt. Takes precedence over WORK — the
|
|
51
|
+
# two markers are never emitted together.
|
|
52
|
+
if [[ "$clean_output" == *MINTREE_ORCHESTRATE:* ]]; then
|
|
53
|
+
local extra=()
|
|
54
|
+
local orch_prompt_file
|
|
55
|
+
orch_prompt_file=$(echo "$clean_output" | grep "MINTREE_ORCHESTRATE_PROMPT_FILE:" | sed 's/.*MINTREE_ORCHESTRATE_PROMPT_FILE://')
|
|
56
|
+
[[ -n "$orch_prompt_file" ]] && extra+=(--prompt-file "$orch_prompt_file")
|
|
57
|
+
[[ -n "$perm_mode" ]] && extra+=(--permission-mode "$perm_mode")
|
|
58
|
+
command mintree orchestrate "${extra[@]}"
|
|
59
|
+
return $?
|
|
60
|
+
fi
|
|
61
|
+
|
|
46
62
|
if [[ "$clean_output" != *MINTREE_WORK:* ]]; then
|
|
47
63
|
return 0
|
|
48
64
|
fi
|
|
49
65
|
local extra=()
|
|
50
66
|
local prompt_file
|
|
51
67
|
prompt_file=$(echo "$clean_output" | grep "MINTREE_WORK_PROMPT_FILE:" | sed 's/.*MINTREE_WORK_PROMPT_FILE://')
|
|
52
|
-
local perm_mode
|
|
53
|
-
perm_mode=$(echo "$clean_output" | grep "MINTREE_PERMISSION_MODE:" | sed 's/.*MINTREE_PERMISSION_MODE://')
|
|
54
68
|
[[ -n "$prompt_file" ]] && extra+=(--prompt-file "$prompt_file")
|
|
55
69
|
[[ -n "$perm_mode" ]] && extra+=(--permission-mode "$perm_mode")
|
|
56
70
|
command mintree worktree work "${extra[@]}"
|
|
@@ -63,7 +77,7 @@ function mintree() {
|
|
|
63
77
|
local exit_code=$?
|
|
64
78
|
|
|
65
79
|
if [[ "$output" == *MINTREE_CD:* ]]; then
|
|
66
|
-
echo "$output" | grep -vE "MINTREE_(CD|WORK|WORK_PROMPT_FILE|PERMISSION_MODE):"
|
|
80
|
+
echo "$output" | grep -vE "MINTREE_(CD|WORK|WORK_PROMPT_FILE|PERMISSION_MODE|ORCHESTRATE|ORCHESTRATE_PROMPT_FILE):"
|
|
67
81
|
local clean_output=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g')
|
|
68
82
|
_mintree_handle_markers "$clean_output"
|
|
69
83
|
else
|
package/shell/init.zsh
CHANGED
|
@@ -53,14 +53,28 @@ function mintree() {
|
|
|
53
53
|
fi
|
|
54
54
|
cd "$target_dir" && echo "Switched to: $target_dir"
|
|
55
55
|
|
|
56
|
+
local perm_mode
|
|
57
|
+
perm_mode=$(echo "$clean_output" | grep "MINTREE_PERMISSION_MODE:" | sed 's/.*MINTREE_PERMISSION_MODE://')
|
|
58
|
+
|
|
59
|
+
# Orchestrate: launch a Claude orchestrator in the repo root (already
|
|
60
|
+
# cd'd above) with the batch prompt. Takes precedence over WORK — the
|
|
61
|
+
# two markers are never emitted together.
|
|
62
|
+
if [[ "$clean_output" == *MINTREE_ORCHESTRATE:* ]]; then
|
|
63
|
+
local extra=()
|
|
64
|
+
local orch_prompt_file
|
|
65
|
+
orch_prompt_file=$(echo "$clean_output" | grep "MINTREE_ORCHESTRATE_PROMPT_FILE:" | sed 's/.*MINTREE_ORCHESTRATE_PROMPT_FILE://')
|
|
66
|
+
[[ -n "$orch_prompt_file" ]] && extra+=(--prompt-file "$orch_prompt_file")
|
|
67
|
+
[[ -n "$perm_mode" ]] && extra+=(--permission-mode "$perm_mode")
|
|
68
|
+
command mintree orchestrate "${extra[@]}"
|
|
69
|
+
return $?
|
|
70
|
+
fi
|
|
71
|
+
|
|
56
72
|
if [[ "$clean_output" != *MINTREE_WORK:* ]]; then
|
|
57
73
|
return 0
|
|
58
74
|
fi
|
|
59
75
|
local extra=()
|
|
60
76
|
local prompt_file
|
|
61
77
|
prompt_file=$(echo "$clean_output" | grep "MINTREE_WORK_PROMPT_FILE:" | sed 's/.*MINTREE_WORK_PROMPT_FILE://')
|
|
62
|
-
local perm_mode
|
|
63
|
-
perm_mode=$(echo "$clean_output" | grep "MINTREE_PERMISSION_MODE:" | sed 's/.*MINTREE_PERMISSION_MODE://')
|
|
64
78
|
[[ -n "$prompt_file" ]] && extra+=(--prompt-file "$prompt_file")
|
|
65
79
|
[[ -n "$perm_mode" ]] && extra+=(--permission-mode "$perm_mode")
|
|
66
80
|
command mintree worktree work "${extra[@]}"
|
|
@@ -75,7 +89,7 @@ function mintree() {
|
|
|
75
89
|
local exit_code=$?
|
|
76
90
|
|
|
77
91
|
if [[ "$output" == *MINTREE_CD:* ]]; then
|
|
78
|
-
echo "$output" | grep -vE "MINTREE_(CD|WORK|WORK_PROMPT_FILE|PERMISSION_MODE):"
|
|
92
|
+
echo "$output" | grep -vE "MINTREE_(CD|WORK|WORK_PROMPT_FILE|PERMISSION_MODE|ORCHESTRATE|ORCHESTRATE_PROMPT_FILE):"
|
|
79
93
|
local clean_output=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g')
|
|
80
94
|
_mintree_handle_markers "$clean_output"
|
|
81
95
|
else
|