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 +16 -7
- package/dist/cli.js +233 -3
- package/dist/index.js +40 -10
- package/dist/lib/projects.d.ts +2 -0
- package/dist/lib/projects.js +6 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +19 -0
- package/package.json +1 -1
- package/prompts/execute.md +1 -0
- package/prompts/review.md +1 -0
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 (
|
|
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
|
-
- **
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
]
|
|
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
|
-
|
|
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,
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/projects.js
CHANGED
|
@@ -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 = {};
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -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
package/prompts/execute.md
CHANGED
|
@@ -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.
|