ticket-to-pr 1.1.0 → 1.2.1

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
@@ -34,7 +34,7 @@ TicketToPR clears that pile. Toss tickets on your Notion board, drag to Review,
34
34
  - **AI scores before AI codes** — every ticket gets an ease/confidence rating and implementation spec before a single line is written. You always decide go/no-go.
35
35
  - **Your codebase, your rules** — Claude reads your project's `CLAUDE.md` and follows your conventions, patterns, and constraints.
36
36
  - **Build validation** — code must pass your build command before anything pushes. No broken PRs.
37
- - **Blocked file guardrails** — configure glob patterns for files the agent must never touch (e.g. migrations, DB schemas). Enforced via prompt + hard post-diff validation.
37
+ - **Blocked file guardrails** — configure glob patterns for files the agent must never touch (e.g. migrations, DB schemas). Enforced via prompt injection into both review and execute agents + hard post-diff validation.
38
38
  - **Full audit trail** — cost, duration, scores, branch name, PR link, and agent comments posted directly on the Notion ticket.
39
39
  - **Cost transparency** — every ticket shows exactly what it cost. Simple tasks run $0.35-0.55.
40
40
  - **Human-in-the-loop** — nothing merges without a developer reviewing the PR.
@@ -210,7 +210,7 @@ ticket-to-pr --dry-run --once
210
210
 
211
211
  | Command / Flag | Behavior |
212
212
  |----------------|----------|
213
- | `init` | Guided setup — validates Notion credentials live, configures projects, writes `.env.local` and `projects.json`. Detects existing config on re-run. |
213
+ | `init` | Guided setup — validates Notion credentials live, auto-detects build commands, generates starter `CLAUDE.md`, configures projects, writes `.env.local` and `projects.json`. Detects existing config on re-run. |
214
214
  | `doctor` | Diagnostic check — verifies environment, Notion connectivity, database schema, tools, and projects |
215
215
  | *(none)* | Continuous polling every 30s |
216
216
  | `--once` | Poll once, wait for agents to finish, exit |
@@ -247,11 +247,15 @@ Step 4: Projects
247
247
  Project name: MyApp
248
248
  Directory: /Users/you/Projects/MyApp
249
249
  ✓ Git repo git@github.com:you/MyApp.git
250
- Build command (optional): npm run build
250
+ Build command (npm run build): auto-detected from package.json
251
251
  Base branch (main):
252
252
  Glob patterns the agent must never touch (e.g. **/migrations/**, prisma/schema.prisma, **/*.sql)
253
253
  Blocked file patterns (optional, comma-separated):
254
254
  Skip automatic PR creation? (N):
255
+ Detected: TypeScript, Next.js, Tailwind CSS ← auto-detected from project files
256
+ Generate starter CLAUDE.md? (Y):
257
+ ✓ Generated CLAUDE.md /Users/you/Projects/MyApp/CLAUDE.md
258
+ Edit it to add project-specific rules and conventions.
255
259
 
256
260
  Add another project? (N):
257
261
 
@@ -265,6 +269,8 @@ Ready!
265
269
  ```
266
270
 
267
271
  - **Blocks on bad config** — invalid Notion tokens and database IDs are rejected and re-prompted (won't save broken credentials)
272
+ - **Auto-detects build command** — reads `package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, or `Makefile` and pre-fills the build command. Press Enter to accept or override.
273
+ - **Generates starter CLAUDE.md** — detects your project stack (language, framework, test runner, CSS, ORM) and offers to generate a `CLAUDE.md` with build commands, code style, and file structure. Both agents read this file for context.
268
274
  - **Re-run safe** — detects existing `.env.local` and `projects.json`, asks "update" or "start fresh"
269
275
  - **Free tier guard** — warns if you configure multiple projects without a Pro license
270
276
  - Masks existing secrets when showing defaults
@@ -386,7 +392,7 @@ tail -f ~/Projects/ticket-to-pr/bridge.log
386
392
  The review agent explores your codebase without modifying anything:
387
393
 
388
394
  - **Tools**: Read, Glob, Grep, Task
389
- - **Context**: Reads your project's `CLAUDE.md` for architecture rules
395
+ - **Context**: Reads your project's `CLAUDE.md` for architecture rules. If `blockedFiles` are configured, the review agent factors those constraints into scoring.
390
396
  - **Output**: Ease score, confidence score, implementation spec, impact report, affected files, risks
391
397
  - **Budget**: $2.00 max, 25 turns max
392
398
  - **Typical cost**: $0.15 - $0.50
@@ -412,7 +418,8 @@ The review agent explores your codebase without modifying anything:
412
418
  The execute agent implements the code based on the spec:
413
419
 
414
420
  - **Tools**: Read, Glob, Grep, Edit, Write + limited Bash (git, build, test only)
415
- - **Cannot**: push, run destructive commands, modify databases, access the web
421
+ - **Dev access** (opt-in): When `devAccess` is enabled, additionally allows `npx tsx`, `node`, `npm run`, `npx vitest`, `npx jest`, `npx prisma`, `python`, and `curl` to localhost/127.0.0.1 only
422
+ - **Cannot**: push, run destructive commands, modify databases, access the web, curl external hosts
416
423
  - **Context**: Reads your project's `CLAUDE.md` for conventions and rules
417
424
  - **Budget**: $15.00 max, 50 turns max
418
425
  - **Typical cost**: $0.20 - $2.00
@@ -490,6 +497,8 @@ Project configuration in `projects.json`:
490
497
  | `projects.<name>.baseBranch` | Optional base branch (e.g. `develop`). Falls back to auto-detected default (`main`/`master`). |
491
498
  | `projects.<name>.blockedFiles` | Optional array of glob patterns the agent must never touch (e.g. `["**/migrations/**", "**/*.sql"]`) |
492
499
  | `projects.<name>.skipPR` | Optional boolean. Set `true` to push the branch but skip automatic PR creation. |
500
+ | `projects.<name>.devAccess` | Optional boolean. Set `true` to let the execute agent run scripts, query DBs, and hit local endpoints. |
501
+ | `projects.<name>.envFile` | Optional env file path relative to project directory (e.g. `.env.local`). Loaded into the agent's environment when set. |
493
502
 
494
503
  ## Project Structure
495
504
 
@@ -533,7 +542,7 @@ Add to `projects.json` (or re-run `ticket-to-pr init`):
533
542
 
534
543
  1. All fields except `directory` are optional — omit any you don't need
535
544
  2. `baseBranch` — which branch to base feature branches on. Auto-detected (`main`/`master`) if omitted.
536
- 3. `blockedFiles` — glob patterns the agent must never touch. Enforced both via prompt injection and a hard post-diff validation before push.
545
+ 3. `blockedFiles` — glob patterns the agent must never touch. Enforced via prompt injection into both review and execute agents, plus a hard post-diff validation before push.
537
546
  4. `skipPR` — set `true` to push branches without creating a PR (useful for repos that use a different PR workflow)
538
547
  5. The directory must be a git repo with an `origin` remote
539
548
  6. If the project has a `CLAUDE.md`, both agents will read it for context
@@ -629,7 +638,7 @@ Add to `projects.json` (or re-run `ticket-to-pr init`):
629
638
  - **Read-only review** — the review agent cannot modify files. It only reads and analyzes.
630
639
  - **Sandboxed execution** — the execute agent has no access to the web, cannot push code, and cannot run destructive commands. TicketToPR handles git operations separately.
631
640
  - **Build gate** — code must pass your build validation before anything is pushed.
632
- - **Blocked file gate** — if `blockedFiles` patterns are configured, a post-diff check runs before push. Any violations abort the run — no code reaches origin.
641
+ - **Blocked file gate** — if `blockedFiles` patterns are configured, they're injected into both the review and execute agent prompts. A hard post-diff check also runs before push. Any violations abort the run — no code reaches origin.
633
642
  - **Human gate** — pull requests require your review and approval before merging.
634
643
 
635
644
  ## Tech Stack
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  import { execSync } from 'node:child_process';
3
- import { readFileSync, existsSync } from 'node:fs';
3
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { mask, shellEscape, writeEnvFile, updateProjectsFile, getDefaultBranch } from './lib/utils.js';
6
6
  import { getProjectNames, getProjectDir, getBaseBranch, getBlockedFiles, getSkipPR } from './lib/projects.js';
@@ -27,6 +27,209 @@ function checkCommand(cmd) {
27
27
  return { ok: false, output: '' };
28
28
  }
29
29
  }
30
+ function detectBuildCommand(dir) {
31
+ // Node.js — check package.json for build/test scripts
32
+ const pkgPath = join(dir, 'package.json');
33
+ if (existsSync(pkgPath)) {
34
+ try {
35
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
36
+ if (pkg.scripts?.build)
37
+ return 'npm run build';
38
+ if (pkg.scripts?.test)
39
+ return 'npm test';
40
+ }
41
+ catch { /* ignore parse errors */ }
42
+ }
43
+ // Rust
44
+ if (existsSync(join(dir, 'Cargo.toml')))
45
+ return 'cargo build';
46
+ // Go
47
+ if (existsSync(join(dir, 'go.mod')))
48
+ return 'go build ./...';
49
+ // Python
50
+ if (existsSync(join(dir, 'pyproject.toml')))
51
+ return 'python -m pytest';
52
+ // Makefile
53
+ if (existsSync(join(dir, 'Makefile')))
54
+ return 'make';
55
+ return undefined;
56
+ }
57
+ function detectProjectStack(dir) {
58
+ const stack = { language: 'JavaScript' };
59
+ // TypeScript detection
60
+ if (existsSync(join(dir, 'tsconfig.json'))) {
61
+ stack.language = 'TypeScript';
62
+ }
63
+ // Rust / Go / Python detection (override language)
64
+ if (existsSync(join(dir, 'Cargo.toml'))) {
65
+ stack.language = 'Rust';
66
+ stack.buildTool = 'cargo';
67
+ stack.testRunner = 'cargo test';
68
+ return stack;
69
+ }
70
+ if (existsSync(join(dir, 'go.mod'))) {
71
+ stack.language = 'Go';
72
+ stack.buildTool = 'go';
73
+ stack.testRunner = 'go test';
74
+ return stack;
75
+ }
76
+ if (existsSync(join(dir, 'pyproject.toml'))) {
77
+ stack.language = 'Python';
78
+ stack.testRunner = 'pytest';
79
+ return stack;
80
+ }
81
+ // Node.js ecosystem — read package.json for framework/tools
82
+ const pkgPath = join(dir, 'package.json');
83
+ if (existsSync(pkgPath)) {
84
+ try {
85
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
86
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
87
+ // Framework
88
+ if (allDeps['next'])
89
+ stack.framework = 'Next.js';
90
+ else if (allDeps['nuxt'])
91
+ stack.framework = 'Nuxt';
92
+ else if (allDeps['@remix-run/node'] || allDeps['@remix-run/react'])
93
+ stack.framework = 'Remix';
94
+ else if (allDeps['express'])
95
+ stack.framework = 'Express';
96
+ else if (allDeps['fastify'])
97
+ stack.framework = 'Fastify';
98
+ else if (allDeps['react'])
99
+ stack.framework = 'React';
100
+ else if (allDeps['vue'])
101
+ stack.framework = 'Vue';
102
+ else if (allDeps['svelte'])
103
+ stack.framework = 'Svelte';
104
+ // Test runner
105
+ if (allDeps['vitest'])
106
+ stack.testRunner = 'vitest';
107
+ else if (allDeps['jest'])
108
+ stack.testRunner = 'jest';
109
+ else if (allDeps['mocha'])
110
+ stack.testRunner = 'mocha';
111
+ // Build tool
112
+ if (allDeps['vite'] && !stack.framework?.includes('Next'))
113
+ stack.buildTool = 'vite';
114
+ else if (allDeps['webpack'])
115
+ stack.buildTool = 'webpack';
116
+ else if (allDeps['esbuild'])
117
+ stack.buildTool = 'esbuild';
118
+ else if (stack.language === 'TypeScript')
119
+ stack.buildTool = 'tsc';
120
+ // CSS
121
+ if (allDeps['tailwindcss'])
122
+ stack.css = 'Tailwind CSS';
123
+ // ORM
124
+ if (allDeps['prisma'] || allDeps['@prisma/client'])
125
+ stack.orm = 'Prisma';
126
+ else if (allDeps['drizzle-orm'])
127
+ stack.orm = 'Drizzle';
128
+ }
129
+ catch { /* ignore parse errors */ }
130
+ }
131
+ // Package manager
132
+ if (existsSync(join(dir, 'bun.lockb')))
133
+ stack.packageManager = 'bun';
134
+ else if (existsSync(join(dir, 'pnpm-lock.yaml')))
135
+ stack.packageManager = 'pnpm';
136
+ else if (existsSync(join(dir, 'yarn.lock')))
137
+ stack.packageManager = 'yarn';
138
+ else if (existsSync(join(dir, 'package-lock.json')))
139
+ stack.packageManager = 'npm';
140
+ return stack;
141
+ }
142
+ function generateClaudeMd(name, stack, buildCmd) {
143
+ const parts = [];
144
+ // Header
145
+ parts.push(`# ${name}\n`);
146
+ // Project overview
147
+ const stackDesc = [stack.language, stack.framework, stack.css, stack.orm].filter(Boolean).join(', ');
148
+ parts.push(`## Project overview`);
149
+ parts.push(`${stackDesc || stack.language} project.\n`);
150
+ // Build & test
151
+ parts.push(`## Build & test`);
152
+ if (buildCmd)
153
+ parts.push(`- Build: \`${buildCmd}\``);
154
+ if (stack.testRunner) {
155
+ const testCmd = stack.testRunner === 'vitest' ? 'npx vitest run'
156
+ : stack.testRunner === 'jest' ? 'npx jest'
157
+ : stack.testRunner === 'pytest' ? 'python -m pytest'
158
+ : stack.testRunner === 'cargo test' ? 'cargo test'
159
+ : stack.testRunner === 'go test' ? 'go test ./...'
160
+ : stack.testRunner;
161
+ parts.push(`- Test: \`${testCmd}\``);
162
+ }
163
+ const pm = stack.packageManager || 'npm';
164
+ if (['TypeScript', 'JavaScript'].includes(stack.language)) {
165
+ parts.push(`- Lint: \`${pm === 'npm' ? 'npm run' : pm} lint\``);
166
+ }
167
+ if (stack.language === 'TypeScript') {
168
+ parts.push(`- Type check: \`npx tsc --noEmit\``);
169
+ }
170
+ parts.push('');
171
+ // Code style
172
+ parts.push(`## Code style`);
173
+ if (stack.framework === 'Next.js') {
174
+ parts.push(`- Use functional components with ${stack.language}`);
175
+ parts.push(`- Prefer server components; add "use client" only when needed`);
176
+ }
177
+ else if (stack.framework === 'React') {
178
+ parts.push(`- Use functional components with hooks`);
179
+ }
180
+ else if (stack.language === 'Rust') {
181
+ parts.push(`- Follow standard Rust conventions (rustfmt, clippy)`);
182
+ }
183
+ else if (stack.language === 'Go') {
184
+ parts.push(`- Follow standard Go conventions (gofmt, go vet)`);
185
+ }
186
+ else if (stack.language === 'Python') {
187
+ parts.push(`- Follow PEP 8 conventions`);
188
+ }
189
+ if (stack.css === 'Tailwind CSS') {
190
+ parts.push(`- Use Tailwind CSS utility classes for styling`);
191
+ }
192
+ if (stack.orm === 'Prisma') {
193
+ parts.push(`- Use Prisma for database access`);
194
+ }
195
+ else if (stack.orm === 'Drizzle') {
196
+ parts.push(`- Use Drizzle ORM for database access`);
197
+ }
198
+ parts.push('');
199
+ // File structure
200
+ parts.push(`## File structure`);
201
+ if (stack.framework === 'Next.js') {
202
+ parts.push(`- app/ — routes and layouts`);
203
+ parts.push(`- components/ — React components`);
204
+ parts.push(`- lib/ — utilities and shared logic`);
205
+ if (stack.orm === 'Prisma')
206
+ parts.push(`- prisma/ — database schema`);
207
+ }
208
+ else if (stack.framework === 'Express' || stack.framework === 'Fastify') {
209
+ parts.push(`- src/ — application source`);
210
+ parts.push(`- routes/ — API routes`);
211
+ parts.push(`- lib/ — utilities and shared logic`);
212
+ }
213
+ else if (stack.language === 'Rust') {
214
+ parts.push(`- src/ — application source`);
215
+ parts.push(`- tests/ — integration tests`);
216
+ }
217
+ else if (stack.language === 'Go') {
218
+ parts.push(`- cmd/ — entrypoints`);
219
+ parts.push(`- internal/ — private packages`);
220
+ parts.push(`- pkg/ — public packages`);
221
+ }
222
+ else if (stack.language === 'Python') {
223
+ parts.push(`- src/ — application source`);
224
+ parts.push(`- tests/ — test files`);
225
+ }
226
+ else {
227
+ parts.push(`- src/ — application source`);
228
+ parts.push(`- lib/ — utilities and shared logic`);
229
+ }
230
+ parts.push('');
231
+ return parts.join('\n');
232
+ }
30
233
  function ask(rl, question, opts) {
31
234
  return new Promise((resolve) => {
32
235
  const suffix = opts?.defaultValue ? ` ${DIM}(${opts.defaultValue})${RESET}` : '';
@@ -476,7 +679,10 @@ export async function runInit() {
476
679
  else {
477
680
  printStatus(null, 'Not a git repo', `${dir} — you can init git later`);
478
681
  }
479
- const buildCmd = await ask(rl, 'Build command (optional)');
682
+ const detectedBuild = detectBuildCommand(dir);
683
+ const buildCmd = await ask(rl, 'Build command' + (detectedBuild ? '' : ' (optional)'), {
684
+ defaultValue: detectedBuild,
685
+ });
480
686
  // Detect default branch for this project
481
687
  const gitExists2 = existsSync(join(dir, '.git'));
482
688
  const detectedBranch = gitExists2 ? getDefaultBranch(dir) : 'main';
@@ -489,7 +695,31 @@ export async function runInit() {
489
695
  : undefined;
490
696
  const skipPRInput = await ask(rl, 'Skip automatic PR creation?', { defaultValue: 'N' });
491
697
  const skipPR = skipPRInput.toLowerCase() === 'y' || skipPRInput.toLowerCase() === 'yes' ? true : undefined;
492
- projects.push({ name, dir, buildCmd: buildCmd || undefined, baseBranch, blockedFiles, skipPR });
698
+ const devAccessInput = await ask(rl, 'Enable dev access (run scripts, query DB, hit endpoints)?', { defaultValue: 'N' });
699
+ const devAccessEnabled = devAccessInput.toLowerCase() === 'y' || devAccessInput.toLowerCase() === 'yes';
700
+ let envFile;
701
+ if (devAccessEnabled) {
702
+ const envCandidates = ['.env.local', '.env.development', '.env'];
703
+ const detected = envCandidates.find(f => existsSync(join(dir, f)));
704
+ envFile = await ask(rl, 'Env file to load', { defaultValue: detected }) || undefined;
705
+ }
706
+ projects.push({ name, dir, buildCmd: buildCmd || undefined, baseBranch, blockedFiles, skipPR, devAccess: devAccessEnabled || undefined, envFile });
707
+ // Offer to generate CLAUDE.md if it doesn't exist
708
+ const claudeMdPath = join(dir, 'CLAUDE.md');
709
+ if (!existsSync(claudeMdPath)) {
710
+ const stack = detectProjectStack(dir);
711
+ console.log(` ${DIM}Detected: ${[stack.language, stack.framework, stack.css, stack.orm].filter(Boolean).join(', ')}${RESET}`);
712
+ const genClaudeMd = await ask(rl, 'Generate starter CLAUDE.md?', { defaultValue: 'Y' });
713
+ if (genClaudeMd.toLowerCase() === 'y' || genClaudeMd.toLowerCase() === 'yes') {
714
+ const content = generateClaudeMd(name, stack, buildCmd || undefined);
715
+ writeFileSync(claudeMdPath, content, 'utf-8');
716
+ printStatus(true, 'Generated CLAUDE.md', claudeMdPath);
717
+ console.log(` ${DIM}Edit it to add project-specific rules and conventions.${RESET}`);
718
+ }
719
+ }
720
+ else {
721
+ printStatus(true, 'CLAUDE.md exists', claudeMdPath);
722
+ }
493
723
  console.log('');
494
724
  const another = await ask(rl, 'Add another project?', { defaultValue: 'N' });
495
725
  addMore = another.toLowerCase() === 'y' || another.toLowerCase() === 'yes';
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { execSync } from 'node:child_process';
3
3
  import { join } from 'node:path';
4
4
  import { query } from '@anthropic-ai/claude-agent-sdk';
5
5
  import { CONFIG, REVIEW_OUTPUT_SCHEMA, isPro } from './config.js';
6
- import { sleep, clamp, extractJsonFromOutput, shellEscape, extractNumber, loadEnv, createWorktree, removeWorktree, getDefaultBranch, validateNoBlockedFiles } from './lib/utils.js';
7
- import { getProjectDir, getProjectNames, getBuildCommand, getBaseBranch, getBlockedFiles, getSkipPR } from './lib/projects.js';
6
+ import { sleep, clamp, extractJsonFromOutput, shellEscape, extractNumber, loadEnv, parseEnvFile, createWorktree, removeWorktree, getDefaultBranch, validateNoBlockedFiles } from './lib/utils.js';
7
+ import { getProjectDir, getProjectNames, getBuildCommand, getBaseBranch, getBlockedFiles, getSkipPR, getDevAccess, getEnvFile } from './lib/projects.js';
8
8
  import { fetchTicketsByStatus, fetchTicketDetails, writeReviewResults, writeExecutionResults, moveTicketStatus, writeFailure, addComment, } from './lib/notion.js';
9
9
  import { PACKAGE_ROOT, CONFIG_DIR } from './lib/paths.js';
10
10
  // Load .env.local from the user's working directory
@@ -51,7 +51,8 @@ async function runReviewAgent(ticket) {
51
51
  }
52
52
  log(CYAN, 'REVIEW', `Starting review for "${ticket.title}" in ${ticket.project}`);
53
53
  const startTime = Date.now();
54
- const prompt = [
54
+ const blockedFiles = getBlockedFiles(ticket.project);
55
+ const promptParts = [
55
56
  reviewPrompt,
56
57
  '',
57
58
  '## Ticket',
@@ -62,7 +63,11 @@ async function runReviewAgent(ticket) {
62
63
  '',
63
64
  '**Page Content**:',
64
65
  ticket.bodyBlocks,
65
- ].join('\n');
66
+ ];
67
+ if (blockedFiles.length > 0) {
68
+ promptParts.push('', '## BLOCKED FILES — CANNOT BE MODIFIED', 'The following file patterns are off-limits. Factor this into your scoring — if the natural implementation would touch these files, lower the ease and confidence scores and note it in risks.', '', ...blockedFiles.map(p => `- \`${p}\``));
69
+ }
70
+ const prompt = promptParts.join('\n');
66
71
  const messages = query({
67
72
  prompt,
68
73
  options: {
@@ -161,6 +166,8 @@ async function runExecuteAgent(ticket) {
161
166
  const baseBranch = getBaseBranch(ticket.project) || getDefaultBranch(projectDir);
162
167
  const blockedFiles = getBlockedFiles(ticket.project);
163
168
  const skipPR = getSkipPR(ticket.project);
169
+ const devAccess = getDevAccess(ticket.project);
170
+ const envFile = getEnvFile(ticket.project);
164
171
  log(MAGENTA, 'EXECUTE', `Starting execution for "${ticket.title}" on branch ${branchName}`);
165
172
  const startTime = Date.now();
166
173
  // Move to In Progress immediately
@@ -191,18 +198,41 @@ async function runExecuteAgent(ticket) {
191
198
  if (blockedFiles.length > 0) {
192
199
  promptParts.push('', '## BLOCKED FILES — DO NOT TOUCH', 'The following file patterns are off-limits. Do NOT create, modify, or delete any files matching these patterns. Violations will cause the entire run to fail.', '', ...blockedFiles.map((p) => `- \`${p}\``));
193
200
  }
201
+ if (devAccess) {
202
+ promptParts.push('', '## DEV ENVIRONMENT ACCESS', 'You have access to run scripts and dev tools in this project. Use this to:', '- Write and run scripts to understand database schema or existing data', '- Hit local API endpoints with curl to understand response shapes', '- Run tests to verify your implementation', '- Use ORM tools (e.g. `npx prisma studio`) to inspect the data model', '', '### Rules', '- Do NOT run database migrations (`prisma migrate`, `db push`, `alembic`, etc.)', '- Do NOT drop, truncate, or bulk-delete data', '- Do NOT make requests to external/production hosts — only localhost and 127.0.0.1', '- Clean up any temporary scripts you create before your final commit', '- If you create test data, document it in a commit message so reviewers know');
203
+ }
194
204
  const prompt = promptParts.join('\n');
205
+ // Build agent environment when envFile is configured
206
+ let agentEnv;
207
+ if (envFile) {
208
+ const projectEnv = parseEnvFile(join(projectDir, envFile));
209
+ agentEnv = { ...process.env, ...projectEnv };
210
+ }
211
+ const baseTools = [
212
+ 'Read', 'Glob', 'Grep', 'Edit', 'Write', 'Task',
213
+ 'Bash(git add:*)', 'Bash(git commit:*)', 'Bash(git status:*)',
214
+ 'Bash(git diff:*)', 'Bash(git log:*)',
215
+ 'Bash(npm run build:*)', 'Bash(npm test:*)', 'Bash(npx tsc:*)',
216
+ ];
217
+ const devTools = [
218
+ 'Bash(npx tsx:*)',
219
+ 'Bash(node:*)',
220
+ 'Bash(npm run:*)',
221
+ 'Bash(npx vitest:*)',
222
+ 'Bash(npx jest:*)',
223
+ 'Bash(npx prisma:*)',
224
+ 'Bash(python:*)',
225
+ 'Bash(curl http://localhost:*)',
226
+ 'Bash(curl http://127.0.0.1:*)',
227
+ ];
228
+ const allowedTools = devAccess ? [...baseTools, ...devTools] : baseTools;
195
229
  const messages = query({
196
230
  prompt,
197
231
  options: {
198
232
  model: CONFIG.EXECUTE_MODEL,
199
233
  cwd: worktreeDir,
200
- allowedTools: [
201
- 'Read', 'Glob', 'Grep', 'Edit', 'Write', 'Task',
202
- 'Bash(git add:*)', 'Bash(git commit:*)', 'Bash(git status:*)',
203
- 'Bash(git diff:*)', 'Bash(git log:*)',
204
- 'Bash(npm run build:*)', 'Bash(npm test:*)', 'Bash(npx tsc:*)',
205
- ],
234
+ allowedTools,
235
+ env: agentEnv,
206
236
  disallowedTools: ['WebFetch', 'WebSearch'],
207
237
  maxTurns: CONFIG.EXECUTE_MAX_TURNS,
208
238
  maxBudgetUsd: CONFIG.EXECUTE_BUDGET_USD,
@@ -4,6 +4,8 @@ export declare function getBuildCommand(name: string): string | undefined;
4
4
  export declare function getBaseBranch(name: string): string | undefined;
5
5
  export declare function getBlockedFiles(name: string): string[];
6
6
  export declare function getSkipPR(name: string): boolean;
7
+ export declare function getDevAccess(name: string): boolean;
8
+ export declare function getEnvFile(name: string): string | undefined;
7
9
  export declare function getAllProjects(): Record<string, string>;
8
10
  /** Reset the in-memory cache (for tests). */
9
11
  export declare function _resetCache(): void;
@@ -33,6 +33,12 @@ export function getBlockedFiles(name) {
33
33
  export function getSkipPR(name) {
34
34
  return load().projects[name]?.skipPR ?? false;
35
35
  }
36
+ export function getDevAccess(name) {
37
+ return load().projects[name]?.devAccess ?? false;
38
+ }
39
+ export function getEnvFile(name) {
40
+ return load().projects[name]?.envFile;
41
+ }
36
42
  export function getAllProjects() {
37
43
  const data = load();
38
44
  const result = {};
@@ -5,6 +5,7 @@ export declare function shellEscape(str: string): string;
5
5
  export declare function extractNumber(ticket: {
6
6
  impact?: string;
7
7
  }, field: string): string;
8
+ export declare function parseEnvFile(filepath: string): Record<string, string>;
8
9
  export declare function loadEnv(filepath: string): void;
9
10
  export declare function mask(str: string): string;
10
11
  export declare function writeEnvFile(filepath: string, updates: Record<string, string>): void;
@@ -15,6 +16,8 @@ export declare function updateProjectsFile(filepath: string, projects: Array<{
15
16
  baseBranch?: string;
16
17
  blockedFiles?: string[];
17
18
  skipPR?: boolean;
19
+ devAccess?: boolean;
20
+ envFile?: string;
18
21
  }>): void;
19
22
  export declare function getDefaultBranch(projectDir: string): string;
20
23
  /** Reset the default branch cache (for tests). */
package/dist/lib/utils.js CHANGED
@@ -61,6 +61,23 @@ export function extractNumber(ticket, field) {
61
61
  }
62
62
  return '?';
63
63
  }
64
+ export function parseEnvFile(filepath) {
65
+ const vars = {};
66
+ try {
67
+ const content = readFileSync(filepath, 'utf-8');
68
+ for (const line of content.split('\n')) {
69
+ const trimmed = line.trim();
70
+ if (!trimmed || trimmed.startsWith('#'))
71
+ continue;
72
+ const eqIndex = trimmed.indexOf('=');
73
+ if (eqIndex === -1)
74
+ continue;
75
+ vars[trimmed.slice(0, eqIndex).trim()] = trimmed.slice(eqIndex + 1).trim();
76
+ }
77
+ }
78
+ catch { /* file doesn't exist */ }
79
+ return vars;
80
+ }
64
81
  export function loadEnv(filepath) {
65
82
  try {
66
83
  const content = readFileSync(filepath, 'utf-8');
@@ -136,6 +153,8 @@ export function updateProjectsFile(filepath, projects) {
136
153
  ...(proj.baseBranch ? { baseBranch: proj.baseBranch } : {}),
137
154
  ...(proj.blockedFiles && proj.blockedFiles.length > 0 ? { blockedFiles: proj.blockedFiles } : {}),
138
155
  ...(proj.skipPR ? { skipPR: proj.skipPR } : {}),
156
+ ...(proj.devAccess ? { devAccess: proj.devAccess } : {}),
157
+ ...(proj.envFile ? { envFile: proj.envFile } : {}),
139
158
  };
140
159
  }
141
160
  writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticket-to-pr",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Drag a Notion ticket, get a pull request. AI-powered dev automation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@ You have been given a ticket with an implementation spec. Follow the spec and im
15
15
  9. Run existing tests if available, but do not add new test files unless the spec explicitly requires it.
16
16
  10. Do not modify files outside the scope of the spec.
17
17
  11. If your prompt includes a "BLOCKED FILES" section, you MUST NOT modify any files matching those patterns. Violations will cause the entire run to fail.
18
+ 12. If your prompt includes a "DEV ENVIRONMENT ACCESS" section, you may run scripts and dev tools as described. Always prefer reading code directly over running scripts when possible.
18
19
 
19
20
  ## When Done
20
21
  Commit all changes with a final commit message summarizing what was done. The commit message should reference the ticket title.
package/prompts/review.md CHANGED
@@ -41,3 +41,4 @@ You MUST end your response with a JSON code block containing exactly these field
41
41
  - List EVERY file that will be touched in affectedFiles.
42
42
  - Read the project's CLAUDE.md if it exists for project-specific rules and architecture.
43
43
  - Explore relevant code files to understand existing patterns before scoring.
44
+ - If the prompt includes a "BLOCKED FILES" section, factor those constraints into your scoring. If the natural implementation would need to modify blocked files, lower the ease score and note the constraint in risks.