mintree 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -126,6 +126,32 @@ 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
+ Two top-level keys in `.mintree/metadata.json` tune how mintree launches Claude — both 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
+ "linear": { "workspaceSlug": "my-team", "teams": [{ "key": "FE" }] }
141
+ }
142
+ ```
143
+
144
+ - **`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.
145
+ - **`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:
146
+
147
+ | Placeholder | Replaced with |
148
+ |-------------|-------------------------------------------------------|
149
+ | `{{id}}` | Issue id — `100` (GitHub) or `FE-123` (Linear) |
150
+ | `{{title}}` | Issue title |
151
+ | `{{url}}` | Issue URL (GitHub issue page / Linear issue link) |
152
+
153
+ 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).
154
+
129
155
  ---
130
156
 
131
157
  ## Daily flow
@@ -14,6 +14,7 @@ import { runCreate, runCreateDetached, } from "../lib/worktreeCreate.js";
14
14
  import { runRemove, runRemoveByPath } from "../lib/worktreeRemove.js";
15
15
  import { buildCreateMarkers, emitMarkers } from "../lib/markers.js";
16
16
  import { readMetadata } from "../lib/metadata.js";
17
+ import { renderPromptTemplate } from "../lib/promptTemplate.js";
17
18
  import { createProvider } from "../lib/providers/index.js";
18
19
  import { loadDashboard } from "../lib/dashboard.js";
19
20
  import { priorityDisplay } from "../lib/priority.js";
@@ -121,11 +122,18 @@ function kebabize(title) {
121
122
  * Default prompt seeded into the overlay's Prompt field when the user opens
122
123
  * `w` for an issue. Single-line on purpose — `ink-text-input` is one-line,
123
124
  * so multi-line templates render weirdly when the user tabs in to edit.
124
- * Provider-aware: GitHub issues get the `#<n>` + `gh issue view` form;
125
- * Linear issues (id like `FE-123`) get the bare id + the issue URL, since
126
- * `gh` can't read Linear and `#` isn't Linear's notation.
125
+ *
126
+ * When the repo configures a `promptTemplate` in `.mintree/metadata.json`,
127
+ * it wins: the `{{id}}`, `{{title}}` and `{{url}}` placeholders are rendered
128
+ * and the result seeds the field. Otherwise we fall back to the built-in,
129
+ * provider-aware default: GitHub issues get the `#<n>` + `gh issue view`
130
+ * form; Linear issues (id like `FE-123`) get the bare id + the issue URL,
131
+ * since `gh` can't read Linear and `#` isn't Linear's notation.
127
132
  */
128
- function defaultPromptForIssue(id, title, url) {
133
+ function defaultPromptForIssue(id, title, url, template) {
134
+ if (template) {
135
+ return renderPromptTemplate(template, { id, title, url });
136
+ }
129
137
  const isTeamPrefixed = /^[A-Z][A-Z0-9_]*-\d+$/.test(id);
130
138
  if (isTeamPrefixed) {
131
139
  return `Empezá a trabajar el ticket ${id} (${title}). Abrí ${url} para leer el contexto completo y seguí las convenciones del repo.`;
@@ -976,7 +984,8 @@ export default function Dashboard() {
976
984
  // (its `branchName`) over the synthesised `<type>/<issue>-<desc>` form —
977
985
  // that's the convention those repos actually follow. Falls back to the
978
986
  // convention form when the issue has no branchName.
979
- const provider = root ? readMetadata(root).provider : undefined;
987
+ const meta = root ? readMetadata(root) : undefined;
988
+ const provider = meta?.provider;
980
989
  const linearBranch = provider === "linear" && issue.issue.branchName ? issue.issue.branchName : null;
981
990
  setState({
982
991
  ...state,
@@ -988,7 +997,7 @@ export default function Dashboard() {
988
997
  type: "feat",
989
998
  desc: kebabize(issue.issue.title) || `issue-${issue.issue.id}`,
990
999
  linearBranch,
991
- prompt: defaultPromptForIssue(issue.issue.id, issue.issue.title, issue.issue.url),
1000
+ prompt: defaultPromptForIssue(issue.issue.id, issue.issue.title, issue.issue.url, meta?.promptTemplate),
992
1001
  field: "branchMode",
993
1002
  error: null,
994
1003
  conventionDoc: root ? findBranchConventionDoc(root) : null,
@@ -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.ZodDefault<z.ZodEnum<{
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
- .default("default")
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: options.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: options.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..." }) })] }));
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)
@@ -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,8 @@ export type Metadata = {
26
27
  issues: Record<string, IssueMeta>;
27
28
  project?: ProjectMeta;
28
29
  linear?: LinearMeta;
30
+ defaultPermissionMode?: PermissionMode;
31
+ promptTemplate?: string;
29
32
  };
30
33
  export declare function readMetadata(repoRoot: string): Metadata;
31
34
  export declare function writeMetadata(repoRoot: string, data: Metadata): void;
@@ -1,11 +1,18 @@
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
+ }
9
16
  function sanitizeLinearTeam(raw) {
10
17
  if (typeof raw !== "object" || raw === null)
11
18
  return undefined;
@@ -77,6 +84,8 @@ export function readMetadata(repoRoot) {
77
84
  const project = sanitizeProject(parsed.project);
78
85
  const provider = sanitizeProvider(parsed.provider);
79
86
  const linear = sanitizeLinear(parsed.linear);
87
+ const defaultPermissionMode = sanitizePermissionMode(parsed.defaultPermissionMode);
88
+ const promptTemplate = sanitizePromptTemplate(parsed.promptTemplate);
80
89
  return {
81
90
  version: 1,
82
91
  issues: typeof parsed.issues === "object" && parsed.issues !== null
@@ -85,6 +94,8 @@ export function readMetadata(repoRoot) {
85
94
  ...(provider ? { provider } : {}),
86
95
  ...(project ? { project } : {}),
87
96
  ...(linear ? { linear } : {}),
97
+ ...(defaultPermissionMode ? { defaultPermissionMode } : {}),
98
+ ...(promptTemplate ? { promptTemplate } : {}),
88
99
  };
89
100
  }
90
101
  catch {
@@ -0,0 +1,18 @@
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;
@@ -0,0 +1,15 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mintree",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "Issue-driven git worktrees + Claude Code sessions for repos with an opinionated SDD+TDD flow.",
5
5
  "license": "MIT",
6
6
  "author": "Martin Mineo <mmineo@canarytechnologies.com>",