mintree 0.4.9 → 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 +51 -2
- package/dist/commands/dashboard.js +170 -31
- package/dist/commands/orchestrate.d.ts +17 -0
- package/dist/commands/orchestrate.js +154 -0
- package/dist/commands/worktree/work.d.ts +1 -1
- package/dist/commands/worktree/work.js +11 -7
- package/dist/lib/markers.d.ts +11 -0
- package/dist/lib/markers.js +16 -0
- package/dist/lib/metadata.d.ts +4 -0
- package/dist/lib/metadata.js +16 -0
- package/dist/lib/promptTemplate.d.ts +45 -0
- package/dist/lib/promptTemplate.js +50 -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
|
@@ -126,6 +126,41 @@ chmod 600 ~/.mintree/credentials.json
|
|
|
126
126
|
|
|
127
127
|
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"`.
|
|
128
128
|
|
|
129
|
+
### Launch behaviour (optional)
|
|
130
|
+
|
|
131
|
+
Three top-level keys in `.mintree/metadata.json` tune how mintree launches Claude — all apply to GitHub and Linear repos alike:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"version": 1,
|
|
136
|
+
"provider": "linear",
|
|
137
|
+
"issues": {},
|
|
138
|
+
"defaultPermissionMode": "auto",
|
|
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.",
|
|
141
|
+
"linear": { "workspaceSlug": "my-team", "teams": [{ "key": "FE" }] }
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- **`defaultPermissionMode`** (`"default"` | `"auto"`): the Claude `--permission-mode` mintree uses when it launches a session — from the dashboard (`w` / `↵`), `worktree work`, or `worktree create --work`. Omitted (or `"default"`) keeps the stricter default mode; `"auto"` starts every session with auto-accept on. The `--permission-mode` / `-m` CLI flag still overrides it per launch.
|
|
146
|
+
- **`promptTemplate`**: the initial message seeded into the dashboard's `w` overlay (the text Claude receives as its first prompt). It replaces mintree's built-in default and supports these placeholders, substituted per issue:
|
|
147
|
+
|
|
148
|
+
| Placeholder | Replaced with |
|
|
149
|
+
|-------------|-------------------------------------------------------|
|
|
150
|
+
| `{{id}}` | Issue id — `100` (GitHub) or `FE-123` (Linear) |
|
|
151
|
+
| `{{title}}` | Issue title |
|
|
152
|
+
| `{{url}}` | Issue URL (GitHub issue page / Linear issue link) |
|
|
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.
|
|
163
|
+
|
|
129
164
|
---
|
|
130
165
|
|
|
131
166
|
## Daily flow
|
|
@@ -138,10 +173,19 @@ mintree dashboard
|
|
|
138
173
|
|
|
139
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.
|
|
140
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
|
+
|
|
141
182
|
| Shortcut | Action |
|
|
142
183
|
|----------|-----------------------------------------------------------------------|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
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 |
|
|
145
189
|
| `w` | Always open the create overlay (type + kebab description) |
|
|
146
190
|
| `d` | Delete the selected worktree (confirmation overlay) |
|
|
147
191
|
| `r` | Manual refresh (auto-refreshes silently every 5 min) |
|
|
@@ -173,6 +217,11 @@ mintree worktree list --pr # also fetch PR status per branch
|
|
|
173
217
|
mintree worktree remove fix/55-bug # drop worktree but keep branch + session_id
|
|
174
218
|
mintree worktree remove fix/55-bug --force # discard uncommitted changes too
|
|
175
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
|
|
176
225
|
```
|
|
177
226
|
|
|
178
227
|
`mt`, `mtw`, `mtn` are shell aliases the wrapper installs for `mintree`, `mintree worktree`, and an interactive "name a branch" shortcut.
|
|
@@ -12,8 +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";
|
|
18
|
+
import { defaultOrchestratorPrompt, renderOrchestratorTemplate, renderPromptTemplate, } from "../lib/promptTemplate.js";
|
|
17
19
|
import { createProvider } from "../lib/providers/index.js";
|
|
18
20
|
import { loadDashboard } from "../lib/dashboard.js";
|
|
19
21
|
import { priorityDisplay } from "../lib/priority.js";
|
|
@@ -32,15 +34,27 @@ function issueMatchesFilter(d, filter) {
|
|
|
32
34
|
return d.issue.id.replace(/\D/g, "").includes(filter);
|
|
33
35
|
}
|
|
34
36
|
function tabIssues(issues, tab, filter = "") {
|
|
35
|
-
|
|
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;
|
|
36
47
|
}
|
|
37
48
|
function currentSelected(s) {
|
|
38
49
|
const displayed = tabIssues(s.issues, s.activeTab, s.filter);
|
|
39
|
-
|
|
40
|
-
return { displayed, selectedIndex };
|
|
50
|
+
return { displayed, selectedIndex: tabIndex(s, s.activeTab) };
|
|
41
51
|
}
|
|
42
52
|
function withSelectedIndex(s, next) {
|
|
43
|
-
|
|
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 };
|
|
44
58
|
}
|
|
45
59
|
// xterm/iTerm/etc switch to the alternate screen buffer with these escape
|
|
46
60
|
// codes. Using the buffer means the dashboard owns the whole window for its
|
|
@@ -121,11 +135,18 @@ function kebabize(title) {
|
|
|
121
135
|
* Default prompt seeded into the overlay's Prompt field when the user opens
|
|
122
136
|
* `w` for an issue. Single-line on purpose — `ink-text-input` is one-line,
|
|
123
137
|
* so multi-line templates render weirdly when the user tabs in to edit.
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
138
|
+
*
|
|
139
|
+
* When the repo configures a `promptTemplate` in `.mintree/metadata.json`,
|
|
140
|
+
* it wins: the `{{id}}`, `{{title}}` and `{{url}}` placeholders are rendered
|
|
141
|
+
* and the result seeds the field. Otherwise we fall back to the built-in,
|
|
142
|
+
* provider-aware default: GitHub issues get the `#<n>` + `gh issue view`
|
|
143
|
+
* form; Linear issues (id like `FE-123`) get the bare id + the issue URL,
|
|
144
|
+
* since `gh` can't read Linear and `#` isn't Linear's notation.
|
|
127
145
|
*/
|
|
128
|
-
function defaultPromptForIssue(id, title, url) {
|
|
146
|
+
function defaultPromptForIssue(id, title, url, template) {
|
|
147
|
+
if (template) {
|
|
148
|
+
return renderPromptTemplate(template, { id, title, url });
|
|
149
|
+
}
|
|
129
150
|
const isTeamPrefixed = /^[A-Z][A-Z0-9_]*-\d+$/.test(id);
|
|
130
151
|
if (isTeamPrefixed) {
|
|
131
152
|
return `Empezá a trabajar el ticket ${id} (${title}). Abrí ${url} para leer el contexto completo y seguí las convenciones del repo.`;
|
|
@@ -175,12 +196,18 @@ function useTerminalSize() {
|
|
|
175
196
|
}, [stdout]);
|
|
176
197
|
return size;
|
|
177
198
|
}
|
|
178
|
-
|
|
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, }) {
|
|
179
205
|
const issuesLabel = ` Issues (${issueCount}) `;
|
|
180
206
|
const worktreesLabel = ` Worktrees (${worktreeCount}) `;
|
|
181
|
-
|
|
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" })] })] }));
|
|
182
209
|
}
|
|
183
|
-
function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
210
|
+
function FooterRow({ phase, overlayKind, latestVersion, listWidth, activeTab, selectedCount, }) {
|
|
184
211
|
if (phase === "error") {
|
|
185
212
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "q quit" }) }));
|
|
186
213
|
}
|
|
@@ -190,6 +217,11 @@ function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
|
190
217
|
if (overlayKind === "remove") {
|
|
191
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" })] }));
|
|
192
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
|
+
}
|
|
193
225
|
// Two-column footer like santree: common navigation/dashboard commands
|
|
194
226
|
// align under the left (list) pane; ticket-specific actions align under
|
|
195
227
|
// the right (detail) pane. Falls back to a single inline row when no
|
|
@@ -228,7 +260,7 @@ function CreateStepIcon({ kind }) {
|
|
|
228
260
|
return _jsx(Text, { color: "yellow", children: "!" });
|
|
229
261
|
return _jsx(Text, { color: "cyan", children: "\u25CB" });
|
|
230
262
|
}
|
|
231
|
-
function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
263
|
+
function IssueListRow({ d, selected, identifierWidth, rowWidth, checkbox, }) {
|
|
232
264
|
// Display the issue id raw (e.g. "FE-123", "100"). The `#` prefix is a
|
|
233
265
|
// GitHub convention that reads as noise for Linear's already-prefixed
|
|
234
266
|
// ids, and dropping it across the board keeps the dashboard provider-
|
|
@@ -242,12 +274,14 @@ function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
|
242
274
|
// priority. See lib/priority.ts.
|
|
243
275
|
const prio = priorityDisplay(d.issue.priority);
|
|
244
276
|
const title = d.issue.title;
|
|
277
|
+
const checkPrefix = checkbox === undefined ? " " : checkbox === "on" ? "[✔] " : "[ ] ";
|
|
278
|
+
const checkColor = checkbox === "on" ? "green" : undefined;
|
|
245
279
|
// The leading-dot Text and the rest are nested under a single Text so the
|
|
246
280
|
// selection background paints the whole row in one contiguous block.
|
|
247
281
|
// `wrap="truncate"` clamps the row to a single line and Ink renders an
|
|
248
282
|
// ellipsis at the cut. The outer Box has a fixed width so the wrap
|
|
249
283
|
// behaviour knows where to truncate.
|
|
250
|
-
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}`] }) }));
|
|
251
285
|
}
|
|
252
286
|
// A project board header — the top level of the grouped issue list. Mirrors
|
|
253
287
|
// the bold project name + dim count seen in the santree dashboard.
|
|
@@ -398,7 +432,7 @@ function windowListRows(listRows, selectedIndex, viewportRows) {
|
|
|
398
432
|
}
|
|
399
433
|
// Renders a single grouped-list row — used for both the sticky header region
|
|
400
434
|
// and the scrollable body so the two stay visually identical.
|
|
401
|
-
function ListRowView({ row, selectedIndex, identifierWidth, width, }) {
|
|
435
|
+
function ListRowView({ row, selectedIndex, identifierWidth, width, selectedIds, }) {
|
|
402
436
|
if (row.kind === "spacer")
|
|
403
437
|
return _jsx(Text, { children: " " });
|
|
404
438
|
if (row.kind === "project") {
|
|
@@ -407,7 +441,7 @@ function ListRowView({ row, selectedIndex, identifierWidth, width, }) {
|
|
|
407
441
|
if (row.kind === "status") {
|
|
408
442
|
return _jsx(StatusHeaderRow, { name: row.name, color: row.color, count: row.count, width: width });
|
|
409
443
|
}
|
|
410
|
-
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 }));
|
|
411
445
|
}
|
|
412
446
|
// Word-wraps a single line at `width` columns, breaking on the last space
|
|
413
447
|
// before the limit when that yields a reasonable cut. Falls back to a hard
|
|
@@ -687,22 +721,42 @@ export default function Dashboard() {
|
|
|
687
721
|
const activeTab = prevReady?.activeTab ?? "issues";
|
|
688
722
|
const previousIssuesIndex = prevReady?.issuesIndex ?? 0;
|
|
689
723
|
const previousWorktreesIndex = prevReady?.worktreesIndex ?? 0;
|
|
724
|
+
const previousOrchestrateIndex = prevReady?.orchestrateIndex ?? 0;
|
|
690
725
|
const previousOverlay = prevReady?.overlay ?? null;
|
|
691
726
|
const previousToast = prevReady?.toast ?? null;
|
|
692
727
|
const previousScroll = prevReady?.detailScrollOffset ?? 0;
|
|
693
728
|
const filter = prevReady?.filter ?? "";
|
|
694
729
|
const issuesList = tabIssues(issues, "issues", filter);
|
|
695
730
|
const worktreesList = tabIssues(issues, "worktrees", filter);
|
|
731
|
+
const orchestrateList = tabIssues(issues, "orchestrate", filter);
|
|
696
732
|
const issuesIndex = Math.min(previousIssuesIndex, Math.max(0, issuesList.length - 1));
|
|
697
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)));
|
|
698
739
|
// Preserve scroll only when the active tab's selected issue still
|
|
699
740
|
// resolves to the same row — clamping or list churn means the user
|
|
700
741
|
// is now reading something else.
|
|
701
742
|
const prevDisplayed = prevReady ? tabIssues(prevReady.issues, activeTab, filter) : [];
|
|
702
|
-
const nextDisplayed = activeTab === "
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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;
|
|
706
760
|
const detailScrollOffset = prevSelectedId !== null && prevSelectedId === nextSelectedId ? previousScroll : 0;
|
|
707
761
|
return {
|
|
708
762
|
phase: "ready",
|
|
@@ -710,6 +764,8 @@ export default function Dashboard() {
|
|
|
710
764
|
activeTab,
|
|
711
765
|
issuesIndex,
|
|
712
766
|
worktreesIndex,
|
|
767
|
+
orchestrateIndex,
|
|
768
|
+
selectedIds,
|
|
713
769
|
detailScrollOffset,
|
|
714
770
|
refreshing: false,
|
|
715
771
|
overlay: previousOverlay,
|
|
@@ -839,7 +895,14 @@ export default function Dashboard() {
|
|
|
839
895
|
// Esc clears an active numeric filter before it falls through to quit —
|
|
840
896
|
// so the user can back out of a search without leaving the dashboard.
|
|
841
897
|
if (key.escape && state.phase === "ready" && state.filter) {
|
|
842
|
-
setState({
|
|
898
|
+
setState({
|
|
899
|
+
...state,
|
|
900
|
+
filter: "",
|
|
901
|
+
issuesIndex: 0,
|
|
902
|
+
worktreesIndex: 0,
|
|
903
|
+
orchestrateIndex: 0,
|
|
904
|
+
detailScrollOffset: 0,
|
|
905
|
+
});
|
|
843
906
|
return;
|
|
844
907
|
}
|
|
845
908
|
if (input === "q" || key.escape || (input === "c" && key.ctrl)) {
|
|
@@ -858,6 +921,7 @@ export default function Dashboard() {
|
|
|
858
921
|
filter: state.filter + input,
|
|
859
922
|
issuesIndex: 0,
|
|
860
923
|
worktreesIndex: 0,
|
|
924
|
+
orchestrateIndex: 0,
|
|
861
925
|
detailScrollOffset: 0,
|
|
862
926
|
});
|
|
863
927
|
return;
|
|
@@ -868,14 +932,17 @@ export default function Dashboard() {
|
|
|
868
932
|
filter: state.filter.slice(0, -1),
|
|
869
933
|
issuesIndex: 0,
|
|
870
934
|
worktreesIndex: 0,
|
|
935
|
+
orchestrateIndex: 0,
|
|
871
936
|
detailScrollOffset: 0,
|
|
872
937
|
});
|
|
873
938
|
return;
|
|
874
939
|
}
|
|
875
940
|
if (key.leftArrow || key.rightArrow) {
|
|
876
|
-
//
|
|
877
|
-
// preserved, so the user returns to the row they left.
|
|
878
|
-
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];
|
|
879
946
|
setState({ ...state, activeTab: next, detailScrollOffset: 0 });
|
|
880
947
|
return;
|
|
881
948
|
}
|
|
@@ -910,6 +977,34 @@ export default function Dashboard() {
|
|
|
910
977
|
void refresh();
|
|
911
978
|
return;
|
|
912
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
|
+
}
|
|
913
1008
|
if (input === "o") {
|
|
914
1009
|
const { displayed, selectedIndex } = currentSelected(state);
|
|
915
1010
|
const issue = displayed[selectedIndex];
|
|
@@ -932,6 +1027,12 @@ export default function Dashboard() {
|
|
|
932
1027
|
return;
|
|
933
1028
|
}
|
|
934
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
|
+
}
|
|
935
1036
|
const { displayed, selectedIndex } = currentSelected(state);
|
|
936
1037
|
const issue = displayed[selectedIndex];
|
|
937
1038
|
if (!issue)
|
|
@@ -968,6 +1069,41 @@ export default function Dashboard() {
|
|
|
968
1069
|
return;
|
|
969
1070
|
}
|
|
970
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
|
+
}
|
|
971
1107
|
function openCreateOverlay(issue) {
|
|
972
1108
|
if (state.phase !== "ready")
|
|
973
1109
|
return;
|
|
@@ -976,7 +1112,8 @@ export default function Dashboard() {
|
|
|
976
1112
|
// (its `branchName`) over the synthesised `<type>/<issue>-<desc>` form —
|
|
977
1113
|
// that's the convention those repos actually follow. Falls back to the
|
|
978
1114
|
// convention form when the issue has no branchName.
|
|
979
|
-
const
|
|
1115
|
+
const meta = root ? readMetadata(root) : undefined;
|
|
1116
|
+
const provider = meta?.provider;
|
|
980
1117
|
const linearBranch = provider === "linear" && issue.issue.branchName ? issue.issue.branchName : null;
|
|
981
1118
|
setState({
|
|
982
1119
|
...state,
|
|
@@ -988,7 +1125,7 @@ export default function Dashboard() {
|
|
|
988
1125
|
type: "feat",
|
|
989
1126
|
desc: kebabize(issue.issue.title) || `issue-${issue.issue.id}`,
|
|
990
1127
|
linearBranch,
|
|
991
|
-
prompt: defaultPromptForIssue(issue.issue.id, issue.issue.title, issue.issue.url),
|
|
1128
|
+
prompt: defaultPromptForIssue(issue.issue.id, issue.issue.title, issue.issue.url, meta?.promptTemplate),
|
|
992
1129
|
field: "branchMode",
|
|
993
1130
|
error: null,
|
|
994
1131
|
conventionDoc: root ? findBranchConventionDoc(root) : null,
|
|
@@ -1231,11 +1368,13 @@ export default function Dashboard() {
|
|
|
1231
1368
|
if (state.phase === "error") {
|
|
1232
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" }) })] }));
|
|
1233
1370
|
}
|
|
1234
|
-
const { issues, refreshing, overlay, toast, activeTab, filter } = state;
|
|
1371
|
+
const { issues, refreshing, overlay, toast, activeTab, filter, selectedIds } = state;
|
|
1235
1372
|
const { displayed, selectedIndex } = currentSelected(state);
|
|
1236
1373
|
const selected = displayed[selectedIndex] ?? null;
|
|
1237
1374
|
const issuesTabCount = issues.reduce((n, d) => (isOrphan(d) ? n : n + 1), 0);
|
|
1238
1375
|
const worktreesTabCount = issues.length - issuesTabCount;
|
|
1376
|
+
// The Orchestrate chip shows how many tickets are currently checked.
|
|
1377
|
+
const orchestrateTabCount = selectedIds.size;
|
|
1239
1378
|
const onOverlayDescChange = (next) => {
|
|
1240
1379
|
if (state.phase !== "ready" || !state.overlay)
|
|
1241
1380
|
return;
|
|
@@ -1283,9 +1422,9 @@ export default function Dashboard() {
|
|
|
1283
1422
|
: displayed.map((d, index) => ({ kind: "issue", d, index }));
|
|
1284
1423
|
const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
|
|
1285
1424
|
const listContentWidth = Math.max(8, listWidth - 4);
|
|
1286
|
-
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
|
|
1287
1426
|
? `No tickets match #${filter} — Esc to clear the filter.`
|
|
1288
|
-
: activeTab === "
|
|
1289
|
-
? "No
|
|
1290
|
-
: "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 })] })] }));
|
|
1291
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
|
+
}
|
|
@@ -3,7 +3,7 @@ export declare const description = "Launch Claude in the current worktree (creat
|
|
|
3
3
|
export declare const options: z.ZodObject<{
|
|
4
4
|
prompt: z.ZodOptional<z.ZodString>;
|
|
5
5
|
promptFile: z.ZodOptional<z.ZodString>;
|
|
6
|
-
permissionMode: z.
|
|
6
|
+
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
7
7
|
default: "default";
|
|
8
8
|
auto: "auto";
|
|
9
9
|
}>>;
|
|
@@ -8,7 +8,7 @@ import { randomUUID } from "crypto";
|
|
|
8
8
|
import { readFileSync, unlinkSync } from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getCurrentBranch, pathExists, } from "../../lib/git.js";
|
|
11
|
-
import { getSessionId, setSessionId } from "../../lib/metadata.js";
|
|
11
|
+
import { getSessionId, setSessionId, readMetadata } from "../../lib/metadata.js";
|
|
12
12
|
import { launchClaude, PERMISSION_MODES } from "../../lib/claude.js";
|
|
13
13
|
export const description = "Launch Claude in the current worktree (creates or resumes a session)";
|
|
14
14
|
export const options = z.object({
|
|
@@ -26,13 +26,13 @@ export const options = z.object({
|
|
|
26
26
|
})),
|
|
27
27
|
permissionMode: z
|
|
28
28
|
.enum(PERMISSION_MODES)
|
|
29
|
-
.
|
|
29
|
+
.optional()
|
|
30
30
|
.describe(option({
|
|
31
|
-
description: `Claude --permission-mode (one of: ${PERMISSION_MODES.join(", ")})
|
|
31
|
+
description: `Claude --permission-mode (one of: ${PERMISSION_MODES.join(", ")}). Defaults to metadata.defaultPermissionMode, else "default".`,
|
|
32
32
|
alias: "m",
|
|
33
33
|
})),
|
|
34
34
|
});
|
|
35
|
-
function resolve(cwd) {
|
|
35
|
+
function resolve(cwd, flagPermissionMode) {
|
|
36
36
|
const repoRoot = findMainRepoRoot(cwd);
|
|
37
37
|
if (!repoRoot) {
|
|
38
38
|
return {
|
|
@@ -93,6 +93,9 @@ function resolve(cwd) {
|
|
|
93
93
|
setSessionId(repoRoot, issueId, sessionId);
|
|
94
94
|
resume = false;
|
|
95
95
|
}
|
|
96
|
+
// Effective permission mode: explicit `--permission-mode` flag wins, else
|
|
97
|
+
// the repo's `metadata.defaultPermissionMode`, else the stricter "default".
|
|
98
|
+
const permissionMode = flagPermissionMode ?? readMetadata(repoRoot).defaultPermissionMode ?? "default";
|
|
96
99
|
return {
|
|
97
100
|
ok: true,
|
|
98
101
|
data: {
|
|
@@ -103,6 +106,7 @@ function resolve(cwd) {
|
|
|
103
106
|
issueId,
|
|
104
107
|
sessionId,
|
|
105
108
|
resume,
|
|
109
|
+
permissionMode,
|
|
106
110
|
},
|
|
107
111
|
};
|
|
108
112
|
}
|
|
@@ -118,7 +122,7 @@ export default function Work({ options }) {
|
|
|
118
122
|
});
|
|
119
123
|
return;
|
|
120
124
|
}
|
|
121
|
-
const result = resolve(process.cwd());
|
|
125
|
+
const result = resolve(process.cwd(), options.permissionMode);
|
|
122
126
|
if (!result.ok) {
|
|
123
127
|
setState({ phase: "error", message: result.message, hint: result.hint });
|
|
124
128
|
return;
|
|
@@ -151,7 +155,7 @@ export default function Work({ options }) {
|
|
|
151
155
|
}
|
|
152
156
|
try {
|
|
153
157
|
const child = launchClaude({
|
|
154
|
-
permissionMode:
|
|
158
|
+
permissionMode: resolved.permissionMode,
|
|
155
159
|
sessionId: resolved.sessionId,
|
|
156
160
|
resume: resolved.resume,
|
|
157
161
|
prompt: effectivePrompt,
|
|
@@ -184,7 +188,7 @@ export default function Work({ options }) {
|
|
|
184
188
|
const { resolved } = state;
|
|
185
189
|
const sessionShort = resolved.sessionId.slice(0, 8);
|
|
186
190
|
const action = resolved.resume ? "resuming" : "starting";
|
|
187
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree worktree work" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", resolved.branch ?? `detached @ ${resolved.worktreeDirName}`] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "session: " }), _jsxs(Text, { children: [sessionShort, "\u2026"] }), _jsxs(Text, { dimColor: true, children: [" (", action, ")"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "permission-mode: " }), _jsx(Text, { children:
|
|
191
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree worktree work" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", resolved.branch ?? `detached @ ${resolved.worktreeDirName}`] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "session: " }), _jsxs(Text, { children: [sessionShort, "\u2026"] }), _jsxs(Text, { dimColor: true, children: [" (", action, ")"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "permission-mode: " }), _jsx(Text, { children: resolved.permissionMode })] }), options.prompt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "initial prompt: " }), _jsxs(Text, { children: ["\"", truncate(options.prompt, 60), "\""] })] })), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "cwd: " }), _jsx(Text, { dimColor: true, children: resolved.worktreePath })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }) })] }));
|
|
188
192
|
}
|
|
189
193
|
function truncate(s, max) {
|
|
190
194
|
if (s.length <= max)
|
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PermissionMode } from "./claude.js";
|
|
1
2
|
export type IssueMeta = {
|
|
2
3
|
base_branch?: string;
|
|
3
4
|
session_id?: string;
|
|
@@ -26,6 +27,9 @@ export type Metadata = {
|
|
|
26
27
|
issues: Record<string, IssueMeta>;
|
|
27
28
|
project?: ProjectMeta;
|
|
28
29
|
linear?: LinearMeta;
|
|
30
|
+
defaultPermissionMode?: PermissionMode;
|
|
31
|
+
promptTemplate?: string;
|
|
32
|
+
orchestratorPromptTemplate?: string;
|
|
29
33
|
};
|
|
30
34
|
export declare function readMetadata(repoRoot: string): Metadata;
|
|
31
35
|
export declare function writeMetadata(repoRoot: string, data: Metadata): void;
|
package/dist/lib/metadata.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { getMetadataPath } from "./git.js";
|
|
3
|
+
import { PERMISSION_MODES } from "./claude.js";
|
|
3
4
|
const EMPTY = { version: 1, issues: {} };
|
|
4
5
|
function sanitizeProvider(raw) {
|
|
5
6
|
if (raw === "github" || raw === "linear")
|
|
6
7
|
return raw;
|
|
7
8
|
return undefined;
|
|
8
9
|
}
|
|
10
|
+
function sanitizePermissionMode(raw) {
|
|
11
|
+
return PERMISSION_MODES.includes(raw) ? raw : undefined;
|
|
12
|
+
}
|
|
13
|
+
function sanitizePromptTemplate(raw) {
|
|
14
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw : undefined;
|
|
15
|
+
}
|
|
16
|
+
function sanitizeOrchestratorPromptTemplate(raw) {
|
|
17
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw : undefined;
|
|
18
|
+
}
|
|
9
19
|
function sanitizeLinearTeam(raw) {
|
|
10
20
|
if (typeof raw !== "object" || raw === null)
|
|
11
21
|
return undefined;
|
|
@@ -77,6 +87,9 @@ export function readMetadata(repoRoot) {
|
|
|
77
87
|
const project = sanitizeProject(parsed.project);
|
|
78
88
|
const provider = sanitizeProvider(parsed.provider);
|
|
79
89
|
const linear = sanitizeLinear(parsed.linear);
|
|
90
|
+
const defaultPermissionMode = sanitizePermissionMode(parsed.defaultPermissionMode);
|
|
91
|
+
const promptTemplate = sanitizePromptTemplate(parsed.promptTemplate);
|
|
92
|
+
const orchestratorPromptTemplate = sanitizeOrchestratorPromptTemplate(parsed.orchestratorPromptTemplate);
|
|
80
93
|
return {
|
|
81
94
|
version: 1,
|
|
82
95
|
issues: typeof parsed.issues === "object" && parsed.issues !== null
|
|
@@ -85,6 +98,9 @@ export function readMetadata(repoRoot) {
|
|
|
85
98
|
...(provider ? { provider } : {}),
|
|
86
99
|
...(project ? { project } : {}),
|
|
87
100
|
...(linear ? { linear } : {}),
|
|
101
|
+
...(defaultPermissionMode ? { defaultPermissionMode } : {}),
|
|
102
|
+
...(promptTemplate ? { promptTemplate } : {}),
|
|
103
|
+
...(orchestratorPromptTemplate ? { orchestratorPromptTemplate } : {}),
|
|
88
104
|
};
|
|
89
105
|
}
|
|
90
106
|
catch {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variables available to a `promptTemplate` in `.mintree/metadata.json`.
|
|
3
|
+
* Kept intentionally small — the template seeds Claude's first message, it
|
|
4
|
+
* doesn't need the whole issue object.
|
|
5
|
+
*/
|
|
6
|
+
export type PromptVars = {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
url: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const PROMPT_PLACEHOLDERS: readonly ["{{id}}", "{{title}}", "{{url}}"];
|
|
12
|
+
/**
|
|
13
|
+
* Renders a `promptTemplate` by substituting the `{{id}}`, `{{title}}` and
|
|
14
|
+
* `{{url}}` placeholders with the issue's values. Whitespace inside the braces
|
|
15
|
+
* is tolerated (`{{ id }}`). Unknown placeholders are left untouched so a typo
|
|
16
|
+
* is visible in the launched prompt instead of silently vanishing.
|
|
17
|
+
*/
|
|
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;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Placeholder tokens a user can drop into their `promptTemplate`. Documented
|
|
2
|
+
// here so the README and any future `init`/help output stay in sync.
|
|
3
|
+
export const PROMPT_PLACEHOLDERS = ["{{id}}", "{{title}}", "{{url}}"];
|
|
4
|
+
/**
|
|
5
|
+
* Renders a `promptTemplate` by substituting the `{{id}}`, `{{title}}` and
|
|
6
|
+
* `{{url}}` placeholders with the issue's values. Whitespace inside the braces
|
|
7
|
+
* is tolerated (`{{ id }}`). Unknown placeholders are left untouched so a typo
|
|
8
|
+
* is visible in the launched prompt instead of silently vanishing.
|
|
9
|
+
*/
|
|
10
|
+
export function renderPromptTemplate(template, vars) {
|
|
11
|
+
return template
|
|
12
|
+
.replace(/\{\{\s*id\s*\}\}/g, vars.id)
|
|
13
|
+
.replace(/\{\{\s*title\s*\}\}/g, vars.title)
|
|
14
|
+
.replace(/\{\{\s*url\s*\}\}/g, vars.url);
|
|
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
|