@kosuke-ai/cli 0.0.44 → 0.0.46
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 +94 -1
- package/claude.costs.json +34 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/dist/kosuke/commands/build.d.ts +5 -1
- package/dist/kosuke/commands/build.d.ts.map +1 -1
- package/dist/kosuke/commands/build.js +297 -3
- package/dist/kosuke/commands/build.js.map +1 -1
- package/dist/kosuke/commands/migrate.d.ts +6 -1
- package/dist/kosuke/commands/migrate.d.ts.map +1 -1
- package/dist/kosuke/commands/migrate.js +124 -37
- package/dist/kosuke/commands/migrate.js.map +1 -1
- package/dist/kosuke/commands/plan.d.ts +4 -20
- package/dist/kosuke/commands/plan.d.ts.map +1 -1
- package/dist/kosuke/commands/plan.js +471 -679
- package/dist/kosuke/commands/plan.js.map +1 -1
- package/dist/kosuke/commands/requirements.js +2 -2
- package/dist/kosuke/commands/requirements.js.map +1 -1
- package/dist/kosuke/commands/review.d.ts +6 -1
- package/dist/kosuke/commands/review.d.ts.map +1 -1
- package/dist/kosuke/commands/review.js +168 -68
- package/dist/kosuke/commands/review.js.map +1 -1
- package/dist/kosuke/commands/serve.d.ts +9 -0
- package/dist/kosuke/commands/serve.d.ts.map +1 -0
- package/dist/kosuke/commands/serve.js +17 -0
- package/dist/kosuke/commands/serve.js.map +1 -0
- package/dist/kosuke/commands/ship.d.ts +6 -1
- package/dist/kosuke/commands/ship.d.ts.map +1 -1
- package/dist/kosuke/commands/ship.js +226 -67
- package/dist/kosuke/commands/ship.js.map +1 -1
- package/dist/kosuke/commands/test.d.ts +6 -1
- package/dist/kosuke/commands/test.d.ts.map +1 -1
- package/dist/kosuke/commands/test.js +171 -73
- package/dist/kosuke/commands/test.js.map +1 -1
- package/dist/kosuke/commands/tickets.d.ts +7 -15
- package/dist/kosuke/commands/tickets.d.ts.map +1 -1
- package/dist/kosuke/commands/tickets.js +14 -58
- package/dist/kosuke/commands/tickets.js.map +1 -1
- package/dist/kosuke/serve/middleware/error-handler.d.ts +13 -0
- package/dist/kosuke/serve/middleware/error-handler.d.ts.map +1 -0
- package/dist/kosuke/serve/middleware/error-handler.js +34 -0
- package/dist/kosuke/serve/middleware/error-handler.js.map +1 -0
- package/dist/kosuke/serve/middleware/validation.d.ts +13 -0
- package/dist/kosuke/serve/middleware/validation.d.ts.map +1 -0
- package/dist/kosuke/serve/middleware/validation.js +39 -0
- package/dist/kosuke/serve/middleware/validation.js.map +1 -0
- package/dist/kosuke/serve/routes/build.d.ts +6 -0
- package/dist/kosuke/serve/routes/build.d.ts.map +1 -0
- package/dist/kosuke/serve/routes/build.js +224 -0
- package/dist/kosuke/serve/routes/build.js.map +1 -0
- package/dist/kosuke/serve/routes/health.d.ts +6 -0
- package/dist/kosuke/serve/routes/health.d.ts.map +1 -0
- package/dist/kosuke/serve/routes/health.js +11 -0
- package/dist/kosuke/serve/routes/health.js.map +1 -0
- package/dist/kosuke/serve/routes/plan.d.ts +6 -0
- package/dist/kosuke/serve/routes/plan.d.ts.map +1 -0
- package/dist/kosuke/serve/routes/plan.js +104 -0
- package/dist/kosuke/serve/routes/plan.js.map +1 -0
- package/dist/kosuke/serve/server.d.ts +16 -0
- package/dist/kosuke/serve/server.d.ts.map +1 -0
- package/dist/kosuke/serve/server.js +63 -0
- package/dist/kosuke/serve/server.js.map +1 -0
- package/dist/kosuke/serve/validation/build.d.ts +41 -0
- package/dist/kosuke/serve/validation/build.d.ts.map +1 -0
- package/dist/kosuke/serve/validation/build.js +20 -0
- package/dist/kosuke/serve/validation/build.js.map +1 -0
- package/dist/kosuke/serve/validation/plan.d.ts +26 -0
- package/dist/kosuke/serve/validation/plan.d.ts.map +1 -0
- package/dist/kosuke/serve/validation/plan.js +15 -0
- package/dist/kosuke/serve/validation/plan.js.map +1 -0
- package/dist/kosuke/types.d.ts +205 -0
- package/dist/kosuke/types.d.ts.map +1 -1
- package/dist/kosuke/utils/claude-agent.d.ts +82 -6
- package/dist/kosuke/utils/claude-agent.d.ts.map +1 -1
- package/dist/kosuke/utils/claude-agent.js +381 -180
- package/dist/kosuke/utils/claude-agent.js.map +1 -1
- package/dist/lib.d.ts +4 -4
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +2 -1
- package/dist/lib.js.map +1 -1
- package/dist/package.json +6 -2
- package/package.json +6 -2
|
@@ -16,14 +16,12 @@
|
|
|
16
16
|
* kosuke plan --prompt="Fix login timeout bug" --dir=./app
|
|
17
17
|
* kosuke plan --prompt="Add notes feature" --no-test # Skip WEB-TEST tickets
|
|
18
18
|
*/
|
|
19
|
-
import
|
|
20
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
19
|
+
import { existsSync, mkdirSync, statSync } from 'fs';
|
|
21
20
|
import { join, resolve } from 'path';
|
|
22
|
-
import {
|
|
23
|
-
import { calculateCost } from '../utils/claude-agent.js';
|
|
21
|
+
import { formatCostBreakdown, runAgent, runAgentStream } from '../utils/claude-agent.js';
|
|
24
22
|
import { askQuestion } from '../utils/interactive-input.js';
|
|
25
23
|
import { logger, setupCancellationHandler } from '../utils/logger.js';
|
|
26
|
-
import {
|
|
24
|
+
import { parseTickets, processAndWriteTickets } from '../utils/tickets-manager.js';
|
|
27
25
|
/**
|
|
28
26
|
* Generate timestamp-based tickets path in tickets/ folder
|
|
29
27
|
* Creates the tickets/ folder if it doesn't exist
|
|
@@ -40,752 +38,431 @@ function generateTicketsPath(cwd) {
|
|
|
40
38
|
.toISOString()
|
|
41
39
|
.replace(/[T:]/g, '-')
|
|
42
40
|
.replace(/\.\d{3}Z$/, '');
|
|
43
|
-
return join(ticketsDir, `${timestamp}.
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Tool definitions for planning - includes file exploration and ticket generation
|
|
47
|
-
*/
|
|
48
|
-
const PLAN_TOOLS = [
|
|
49
|
-
{
|
|
50
|
-
name: 'read_file',
|
|
51
|
-
description: 'Read the contents of a file. Use this to explore the codebase and understand existing patterns, conventions, and implementations.',
|
|
52
|
-
input_schema: {
|
|
53
|
-
type: 'object',
|
|
54
|
-
properties: {
|
|
55
|
-
path: {
|
|
56
|
-
type: 'string',
|
|
57
|
-
description: 'Path to the file to read (relative to project root)',
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
required: ['path'],
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: 'list_directory',
|
|
65
|
-
description: 'List files and directories in a given path. Use this to explore the project structure.',
|
|
66
|
-
input_schema: {
|
|
67
|
-
type: 'object',
|
|
68
|
-
properties: {
|
|
69
|
-
path: {
|
|
70
|
-
type: 'string',
|
|
71
|
-
description: 'Path to the directory to list (relative to project root, use "." for root)',
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
required: ['path'],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: 'glob_search',
|
|
79
|
-
description: 'Find files matching a glob pattern. Use this to find specific file types or locate files by name pattern.',
|
|
80
|
-
input_schema: {
|
|
81
|
-
type: 'object',
|
|
82
|
-
properties: {
|
|
83
|
-
pattern: {
|
|
84
|
-
type: 'string',
|
|
85
|
-
description: 'Glob pattern to match (e.g., "**/*.ts", "lib/db/**/*.ts", "**/schema*.ts")',
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
required: ['pattern'],
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: 'write_tickets',
|
|
93
|
-
description: 'Create tickets.json file with implementation tickets. Use this when all clarification questions have been answered and you have enough information to create actionable tickets.',
|
|
94
|
-
input_schema: {
|
|
95
|
-
type: 'object',
|
|
96
|
-
properties: {
|
|
97
|
-
tickets: {
|
|
98
|
-
type: 'array',
|
|
99
|
-
description: 'Array of tickets to create',
|
|
100
|
-
items: {
|
|
101
|
-
type: 'object',
|
|
102
|
-
properties: {
|
|
103
|
-
id: {
|
|
104
|
-
type: 'string',
|
|
105
|
-
description: 'Ticket ID with prefix: PLAN-SCHEMA- for database, PLAN-ENGINE- for Python microservice, PLAN-BACKEND- for API, PLAN-FRONTEND- for UI, PLAN-WEB-TEST- for E2E tests',
|
|
106
|
-
},
|
|
107
|
-
title: {
|
|
108
|
-
type: 'string',
|
|
109
|
-
description: 'Short descriptive title',
|
|
110
|
-
},
|
|
111
|
-
description: {
|
|
112
|
-
type: 'string',
|
|
113
|
-
description: 'Detailed description with acceptance criteria, implementation notes, and technical requirements based on codebase analysis',
|
|
114
|
-
},
|
|
115
|
-
type: {
|
|
116
|
-
type: 'string',
|
|
117
|
-
enum: ['schema', 'engine', 'backend', 'frontend', 'test'],
|
|
118
|
-
description: 'Ticket type: schema (database), engine (Python microservice), backend (API), frontend (UI), test (E2E)',
|
|
119
|
-
},
|
|
120
|
-
estimatedEffort: {
|
|
121
|
-
type: 'number',
|
|
122
|
-
description: 'Effort estimate 1-10',
|
|
123
|
-
},
|
|
124
|
-
category: {
|
|
125
|
-
type: 'string',
|
|
126
|
-
description: 'Feature category (e.g., auth, billing, tasks, ui)',
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
required: ['id', 'title', 'description', 'type', 'estimatedEffort'],
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
required: ['tickets'],
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
];
|
|
137
|
-
/**
|
|
138
|
-
* Read CLAUDE.md from project directory if it exists
|
|
139
|
-
*/
|
|
140
|
-
function readClaudeMd(cwd) {
|
|
141
|
-
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
142
|
-
if (existsSync(claudeMdPath)) {
|
|
143
|
-
try {
|
|
144
|
-
return readFileSync(claudeMdPath, 'utf-8');
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return null;
|
|
41
|
+
return join(ticketsDir, `${timestamp}.tickets.json`);
|
|
151
42
|
}
|
|
152
43
|
/**
|
|
153
44
|
* Build system prompt for plan command
|
|
154
|
-
*
|
|
45
|
+
*
|
|
46
|
+
* Two-phase workflow:
|
|
47
|
+
* - Phase 1: Ask clarification questions (non-technical, user-focused)
|
|
48
|
+
* - Phase 2: Generate tickets via write_tickets tool
|
|
49
|
+
*
|
|
155
50
|
* @param noTest - If true, exclude WEB-TEST tickets from generation
|
|
156
51
|
*/
|
|
157
|
-
function buildPlanSystemPrompt(
|
|
158
|
-
const
|
|
159
|
-
?
|
|
160
|
-
|
|
161
|
-
**PROJECT CONTEXT (from CLAUDE.md):**
|
|
52
|
+
function buildPlanSystemPrompt(noTest = false) {
|
|
53
|
+
const testGuidelines = noTest
|
|
54
|
+
? ''
|
|
55
|
+
: `
|
|
162
56
|
|
|
163
|
-
|
|
57
|
+
**E2E Test Tickets:**
|
|
58
|
+
- Find test users in seed files (\`lib/db/seed.ts\`)
|
|
59
|
+
- Pattern: \`*+kosuke_test@example.com\` → OTP: \`424242\`
|
|
60
|
+
- Required: Test credentials, numbered steps, clear element descriptions, acceptance criteria`;
|
|
61
|
+
return `# ROLE: TICKET PLANNER
|
|
164
62
|
|
|
165
|
-
|
|
166
|
-
`
|
|
167
|
-
: '';
|
|
168
|
-
return `You are an expert software architect helping plan implementation tickets for a feature or bug fix.
|
|
63
|
+
**YOUR PRIMARY OBJECTIVE:** Create implementation tickets that developers will use to build the feature. You analyze requirements and break them into structured, actionable tickets.
|
|
169
64
|
|
|
170
|
-
**YOUR PRIMARY OBJECTIVE:** Gather enough information through clarification questions to create actionable implementation tickets that can be processed by an automated build system.
|
|
171
|
-
${claudeSection}
|
|
172
65
|
**Your Workflow:**
|
|
173
66
|
|
|
174
|
-
1
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
67
|
+
**Phase 1:** Ask clarification questions (non-technical, user-focused)
|
|
68
|
+
**Phase 2:** Output raw JSON tickets array
|
|
69
|
+
|
|
70
|
+
**Critical Rules:**
|
|
71
|
+
- NEVER start implementation - you only create tickets
|
|
72
|
+
- NEVER create/edit files, install packages, or use Task tool
|
|
73
|
+
- ALWAYS explore codebase (Read/Grep/Glob) to understand patterns before asking questions
|
|
74
|
+
- YOU decide technical implementation details based on existing codebase
|
|
75
|
+
|
|
76
|
+
## Phase 1: Ask Clarification Questions
|
|
179
77
|
|
|
180
|
-
|
|
78
|
+
When a user describes a feature request, analyze the codebase (Read/Grep/Glob) to understand existing patterns, then ask **ONLY business/UX questions** (not technical implementation questions).
|
|
79
|
+
|
|
80
|
+
**Your first response MUST use this exact format:**
|
|
181
81
|
|
|
182
82
|
---
|
|
183
83
|
## Understanding Your Request
|
|
184
|
-
|
|
185
|
-
[Brief summary of what you understood]
|
|
84
|
+
[Brief business summary - what the user wants to achieve, NOT implementation details]
|
|
186
85
|
|
|
187
86
|
## Clarification Questions
|
|
188
87
|
|
|
189
|
-
For each
|
|
88
|
+
For each clarification needed, provide BOTH a question AND a recommended approach:
|
|
190
89
|
|
|
191
90
|
1. **[Topic]**
|
|
192
|
-
- Question: [
|
|
193
|
-
- 💡 Recommendation: [
|
|
91
|
+
- Question: [Non-technical user question]
|
|
92
|
+
- 💡 Recommendation: [Default choice that simplifies scope]
|
|
93
|
+
|
|
94
|
+
2. **[Topic]**
|
|
95
|
+
- Question: [Non-technical user question]
|
|
96
|
+
- 💡 Recommendation: [Default choice that simplifies scope]
|
|
194
97
|
|
|
195
|
-
|
|
196
|
-
- Question: [User-focused question - NOT technical]
|
|
197
|
-
- 💡 Recommendation: [Simple, practical default choice]
|
|
98
|
+
(Add as many questions as needed)
|
|
198
99
|
|
|
199
|
-
**Quick Option:**
|
|
100
|
+
**Quick Response Option:** You can reply "go for recommendations" to accept all recommended approaches at once.
|
|
200
101
|
---
|
|
201
102
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
103
|
+
**ALLOWED QUESTIONS (User/Business Focus):**
|
|
104
|
+
- What data to show/hide, empty state behavior
|
|
105
|
+
- Error messages users see
|
|
106
|
+
- Permission rules (who can do what)
|
|
107
|
+
- User workflow and steps
|
|
207
108
|
|
|
208
|
-
|
|
109
|
+
**FORBIDDEN QUESTIONS (Technical - YOU decide these):**
|
|
110
|
+
- Which library to use (jsPDF, PDFKit, etc.)
|
|
111
|
+
- Backend vs client-side generation
|
|
112
|
+
- API design (REST, tRPC, GraphQL)
|
|
113
|
+
- Database queries or ORM choice
|
|
114
|
+
- Component structure or styling
|
|
115
|
+
- Performance optimization techniques
|
|
209
116
|
|
|
210
|
-
**
|
|
211
|
-
- \`PLAN-SCHEMA-N\`: Database schema changes (Drizzle ORM migrations)
|
|
212
|
-
- \`PLAN-ENGINE-N\`: Python microservice (FastAPI endpoints)
|
|
213
|
-
- \`PLAN-BACKEND-N\`: API/server-side logic (tRPC, server actions)
|
|
214
|
-
- \`PLAN-FRONTEND-N\`: UI components and pages (React, Next.js)
|
|
117
|
+
**YOU decide all technical choices** based on existing codebase patterns.
|
|
215
118
|
|
|
216
|
-
**
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
? ''
|
|
220
|
-
: `
|
|
221
|
-
- \`PLAN-WEB-TEST-N\`: E2E tests (Playwright, browser testing)`}
|
|
119
|
+
**Language Reminders:**
|
|
120
|
+
- ⚠️ NEVER say: "I'll implement", "Let me build", "Now I'll create the files"
|
|
121
|
+
- ✅ INSTEAD say: "I'll create tickets for", "The tickets will include"
|
|
222
122
|
|
|
223
|
-
|
|
224
|
-
1. PLAN-SCHEMA tickets first (database changes)
|
|
225
|
-
2. PLAN-ENGINE tickets (Python microservice - so backend can call it)
|
|
226
|
-
3. PLAN-BACKEND tickets (API layer)
|
|
227
|
-
4. PLAN-FRONTEND tickets (UI layer)${noTest
|
|
228
|
-
? ''
|
|
229
|
-
: `
|
|
230
|
-
5. PLAN-WEB-TEST tickets last (validate everything works)
|
|
231
|
-
|
|
232
|
-
**WEB TEST TICKETS - Playwright MCP E2E Tests:**
|
|
233
|
-
|
|
234
|
-
Web test tickets are executed by Playwright MCP with Claude AI. Follow these guidelines:
|
|
235
|
-
|
|
236
|
-
**Test User Discovery:**
|
|
237
|
-
- Read seed files (lib/db/seed.ts or src/lib/db/seed.ts) to find test users
|
|
238
|
-
- Pattern: Any email ending with "+kosuke_test@example.com" uses OTP code "424242"
|
|
239
|
-
- Example: john+kosuke_test@example.com → OTP: 424242
|
|
240
|
-
|
|
241
|
-
**Each Web Test Ticket MUST Include:**
|
|
242
|
-
1. **Test User Credentials** (at the top)
|
|
243
|
-
- Email addresses of test users
|
|
244
|
-
- OTP code: 424242
|
|
245
|
-
- User roles if applicable
|
|
246
|
-
|
|
247
|
-
2. **Test Steps** (numbered, detailed natural language)
|
|
248
|
-
- Navigation: "Navigate to /sign-in"
|
|
249
|
-
- Interactions: "Click button labeled 'New Task'"
|
|
250
|
-
- Inputs: "Enter 'Test Task' in title field"
|
|
251
|
-
- Expected outcomes: "Expected: Task appears in list"
|
|
252
|
-
- Use CLEAR element descriptions (button text, labels)
|
|
253
|
-
|
|
254
|
-
3. **Acceptance Criteria**
|
|
255
|
-
- Final expected state
|
|
256
|
-
- Data validation points
|
|
257
|
-
|
|
258
|
-
**Authentication Steps Template:**
|
|
259
|
-
1. Navigate to /sign-in
|
|
260
|
-
2. Enter email: {test_user}+kosuke_test@example.com
|
|
261
|
-
3. Click "Send Code" button
|
|
262
|
-
4. Enter OTP: 424242
|
|
263
|
-
5. Click "Verify" button
|
|
264
|
-
6. Expected: Redirected to main app`}
|
|
265
|
-
|
|
266
|
-
**CRITICAL RULES FOR QUESTIONS:**
|
|
267
|
-
- Questions must be NON-TECHNICAL and USER-FOCUSED
|
|
268
|
-
- Focus ONLY on user experience, behavior, and business logic
|
|
269
|
-
- YOU decide all technical/algorithmic details (libraries, caching, performance, architecture)
|
|
270
|
-
- Never ask about: URLs, database design, APIs, algorithms, processing timing, or implementation approach
|
|
271
|
-
- Include technical decisions in ticket DESCRIPTIONS, not in questions to users
|
|
123
|
+
## Phase 2: Generate Tickets
|
|
272
124
|
|
|
273
|
-
|
|
274
|
-
- ❌ BAD: "Should we cache results or process on-demand?"
|
|
275
|
-
- ❌ BAD: "Should this use Python or TypeScript?"
|
|
276
|
-
- ✅ GOOD: "Should invoices be per-user or shared per company?"
|
|
277
|
-
- ✅ GOOD: "For empty descriptions, show neutral mood or hide it?"
|
|
125
|
+
Once the user answers questions OR says "go for recommendations", immediately output a raw JSON array of tickets.
|
|
278
126
|
|
|
279
|
-
**
|
|
280
|
-
-
|
|
281
|
-
-
|
|
282
|
-
-
|
|
127
|
+
**Critical:**
|
|
128
|
+
- DO NOT say "Now I'll implement" or show "Implementation Plan" - you're creating TICKETS, not implementing
|
|
129
|
+
- DO NOT read more files after Phase 1 completes
|
|
130
|
+
- Your ENTIRE response = JSON array (no markdown wrapping, no explanations)
|
|
131
|
+
- Output starts with "[" and ends with "]"
|
|
283
132
|
|
|
284
|
-
**
|
|
133
|
+
**Required JSON Format:**
|
|
285
134
|
[
|
|
286
|
-
{
|
|
287
|
-
"id": "PLAN-SCHEMA-1",
|
|
288
|
-
"title": "Create tasks schema",
|
|
289
|
-
"description": "Create database schema for tasks feature:\\n- Create taskStatusEnum: 'todo', 'in_progress', 'done'\\n- Create tasks table with userId foreign key\\n- Export inferred types\\n\\n**Acceptance Criteria:**\\n- Tasks table created\\n- Enums defined at database level\\n- Migrations generated\\n\\n**Technical Notes:**\\n- Follow existing schema patterns in lib/db/schema/\\n- Use Drizzle ORM conventions",
|
|
290
|
-
"type": "schema",
|
|
291
|
-
"estimatedEffort": 4,
|
|
292
|
-
"category": "tasks"
|
|
293
|
-
},
|
|
294
135
|
{
|
|
295
136
|
"id": "PLAN-BACKEND-1",
|
|
296
|
-
"title": "
|
|
297
|
-
"description": "Create
|
|
137
|
+
"title": "Add PDF export endpoint to orders router",
|
|
138
|
+
"description": "Create tRPC endpoint that generates PDF.\\n\\n**Acceptance Criteria:**\\n- Accepts filter parameters\\n- Returns PDF file\\n- Respects filters",
|
|
298
139
|
"type": "backend",
|
|
299
140
|
"estimatedEffort": 5,
|
|
300
|
-
"
|
|
141
|
+
"status": "Todo",
|
|
142
|
+
"category": "orders"
|
|
301
143
|
},
|
|
302
144
|
{
|
|
303
145
|
"id": "PLAN-FRONTEND-1",
|
|
304
|
-
"title": "
|
|
305
|
-
"description": "
|
|
146
|
+
"title": "Add PDF export button to orders table",
|
|
147
|
+
"description": "Add button to toolbar.\\n\\n**Acceptance Criteria:**\\n- Button visible\\n- Downloads PDF\\n- Shows loading state",
|
|
306
148
|
"type": "frontend",
|
|
307
|
-
"estimatedEffort":
|
|
308
|
-
"
|
|
309
|
-
|
|
149
|
+
"estimatedEffort": 4,
|
|
150
|
+
"status": "Todo",
|
|
151
|
+
"category": "orders"
|
|
152
|
+
}${noTest
|
|
153
|
+
? ''
|
|
154
|
+
: `,
|
|
310
155
|
{
|
|
311
156
|
"id": "PLAN-WEB-TEST-1",
|
|
312
|
-
"title": "E2E:
|
|
313
|
-
"description": "**Test User
|
|
157
|
+
"title": "E2E: Test PDF export with filters",
|
|
158
|
+
"description": "**Test User:** john+kosuke_test@example.com (OTP: 424242)\\n\\n**Steps:**\\n1. Sign in\\n2. Apply filters\\n3. Click PDF export\\n4. Verify download\\n\\n**Acceptance Criteria:**\\n- PDF downloads\\n- Contains filtered data",
|
|
314
159
|
"type": "test",
|
|
315
|
-
"estimatedEffort":
|
|
316
|
-
"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (entry.startsWith('.') && entry !== '.env.example')
|
|
363
|
-
continue;
|
|
364
|
-
if (ignoreDirs.includes(entry))
|
|
365
|
-
continue;
|
|
366
|
-
const entryPath = join(fullPath, entry);
|
|
367
|
-
try {
|
|
368
|
-
const entryStat = statSync(entryPath);
|
|
369
|
-
if (entryStat.isDirectory()) {
|
|
370
|
-
items.push(`📁 ${entry}/`);
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
items.push(`📄 ${entry}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
catch {
|
|
377
|
-
items.push(`❓ ${entry}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
console.log(`\n 📂 Listing: ${dirPath}`);
|
|
381
|
-
return { success: true, content: items.join('\n') || '(empty directory)' };
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
385
|
-
return { success: false, content: `Error listing directory: ${msg}` };
|
|
386
|
-
}
|
|
160
|
+
"estimatedEffort": 3,
|
|
161
|
+
"status": "Todo",
|
|
162
|
+
"category": "orders"
|
|
163
|
+
}`}
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
**CRITICAL - Exact Ticket ID Format (MUST MATCH):**
|
|
167
|
+
- PLAN-SCHEMA-1, PLAN-SCHEMA-2, ... → type: "schema"
|
|
168
|
+
- PLAN-ENGINE-1, PLAN-ENGINE-2, ... → type: "engine"
|
|
169
|
+
- PLAN-BACKEND-1, PLAN-BACKEND-2, ... → type: "backend"
|
|
170
|
+
- PLAN-FRONTEND-1, PLAN-FRONTEND-2, ... → type: "frontend"${noTest ? '' : '\n- PLAN-WEB-TEST-1, PLAN-WEB-TEST-2, ... → type: "test"'}
|
|
171
|
+
|
|
172
|
+
**Required Fields (every ticket):**
|
|
173
|
+
- id: Exact format above (NOT "PLAN-TEST-1", use "PLAN-WEB-TEST-1")
|
|
174
|
+
- title: Short descriptive title
|
|
175
|
+
- description: Details with **Acceptance Criteria:** section (use \\n for newlines)
|
|
176
|
+
- type: MUST be one of: "schema", "engine", "backend", "frontend"${noTest ? '' : ', "test"'}
|
|
177
|
+
- estimatedEffort: Integer from 1 to 10
|
|
178
|
+
- status: ALWAYS "Todo"
|
|
179
|
+
- category: Optional (e.g., "orders", "auth", "billing")
|
|
180
|
+
|
|
181
|
+
**Dependency order:** SCHEMA → ENGINE → BACKEND → FRONTEND${noTest ? '' : ' → WEB-TEST'}
|
|
182
|
+
${testGuidelines}
|
|
183
|
+
|
|
184
|
+
🚨 **WHEN USER SAYS "go for recommendations" OR ANSWERS YOUR QUESTIONS:**
|
|
185
|
+
|
|
186
|
+
YOU ARE NOW IN PHASE 2. EXPLORATION IS OVER.
|
|
187
|
+
|
|
188
|
+
DO NOT:
|
|
189
|
+
- Read more files
|
|
190
|
+
- Search for packages
|
|
191
|
+
- Say "Now I'll implement" or "Let me create"
|
|
192
|
+
- Use Task tool
|
|
193
|
+
- Show "Implementation Plan"
|
|
194
|
+
- Add any text before or after the JSON
|
|
195
|
+
|
|
196
|
+
IMMEDIATELY OUTPUT THE JSON ARRAY. Your ENTIRE response = JSON array starting with "[" and ending with "]".
|
|
197
|
+
|
|
198
|
+
**Examples:**
|
|
199
|
+
|
|
200
|
+
❌ WRONG: \`Perfect! Now I'll implement... [shows implementation plan]\`
|
|
201
|
+
❌ WRONG: \`Great! I'll create the tickets. \\\`\\\`\\\`json [...] \\\`\\\`\\\`\` (markdown wrapping)
|
|
202
|
+
❌ WRONG: \`Now I'll update the files...\`
|
|
203
|
+
|
|
204
|
+
✅ CORRECT: \`[{"id": "PLAN-BACKEND-1", "title": "Add PDF export endpoint", ...}]\`
|
|
205
|
+
|
|
206
|
+
Just the raw JSON array. No explanatory text. No markdown code fences. No "Now I'll implement" language.`;
|
|
387
207
|
}
|
|
388
208
|
/**
|
|
389
|
-
*
|
|
209
|
+
* Shared setup for plan agent configuration
|
|
390
210
|
*/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**', '.next/**', '__pycache__/**'],
|
|
398
|
-
});
|
|
399
|
-
if (files.length === 0) {
|
|
400
|
-
return { success: true, content: `No files found matching: ${pattern}` };
|
|
401
|
-
}
|
|
402
|
-
// Limit results
|
|
403
|
-
const maxResults = 50;
|
|
404
|
-
const truncated = files.length > maxResults;
|
|
405
|
-
const displayFiles = files.slice(0, maxResults);
|
|
406
|
-
console.log(`\n 🔍 Found ${files.length} file(s) matching: ${pattern}`);
|
|
407
|
-
let content = displayFiles.join('\n');
|
|
408
|
-
if (truncated) {
|
|
409
|
-
content += `\n\n...[showing ${maxResults} of ${files.length} files]`;
|
|
410
|
-
}
|
|
411
|
-
return { success: true, content };
|
|
211
|
+
function createPlanAgentConfig(options) {
|
|
212
|
+
const { prompt, directory, noTest = false, resume } = options;
|
|
213
|
+
// Validate directory
|
|
214
|
+
const cwd = directory ? resolve(directory) : process.cwd();
|
|
215
|
+
if (!existsSync(cwd)) {
|
|
216
|
+
return { error: `Directory not found: ${cwd}` };
|
|
412
217
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return {
|
|
218
|
+
const stats = statSync(cwd);
|
|
219
|
+
if (!stats.isDirectory()) {
|
|
220
|
+
return { error: `Path is not a directory: ${cwd}` };
|
|
416
221
|
}
|
|
222
|
+
const ticketsPath = generateTicketsPath(cwd);
|
|
223
|
+
const systemPrompt = buildPlanSystemPrompt(noTest);
|
|
224
|
+
const config = {
|
|
225
|
+
systemPrompt,
|
|
226
|
+
cwd,
|
|
227
|
+
maxTurns: 40, // Increased for complex planning sessions with multiple files to analyze
|
|
228
|
+
verbosity: 'verbose',
|
|
229
|
+
permissionMode: 'bypassPermissions',
|
|
230
|
+
// Restrict to read-only tools only
|
|
231
|
+
// Block all code editing, execution, task management, and agent control tools
|
|
232
|
+
disallowedTools: [
|
|
233
|
+
'Edit', // File editing
|
|
234
|
+
'Write', // File creation
|
|
235
|
+
'Delete', // File deletion
|
|
236
|
+
'NotebookEdit', // Notebook editing
|
|
237
|
+
'Bash', // Shell execution
|
|
238
|
+
'Task', // Sub-agent spawning (prevents implementation subtasks)
|
|
239
|
+
'TodoWrite', // Task management (implementation mode)
|
|
240
|
+
'ExitPlanMode', // Mode transitions
|
|
241
|
+
],
|
|
242
|
+
...(resume && { resume }),
|
|
243
|
+
};
|
|
244
|
+
return { config, ticketsPath, cwd, prompt };
|
|
417
245
|
}
|
|
418
246
|
/**
|
|
419
|
-
*
|
|
420
|
-
* Returns parsed tickets for later validation - does NOT write to file
|
|
247
|
+
* Plan core stream for server (async generator, no logging)
|
|
421
248
|
*/
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}));
|
|
435
|
-
// Sort tickets by processing order (using shared utility)
|
|
436
|
-
const sortedTickets = sortTicketsByOrder(tickets);
|
|
437
|
-
console.log(`\n📋 Generated ${sortedTickets.length} ticket(s) - validating...`);
|
|
438
|
-
return {
|
|
439
|
-
success: true,
|
|
440
|
-
message: `Generated ${sortedTickets.length} tickets - will validate and save after confirmation`,
|
|
441
|
-
tickets: sortedTickets,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
446
|
-
console.error(`\n❌ Failed to parse tickets: ${errorMessage}`);
|
|
447
|
-
return {
|
|
448
|
-
success: false,
|
|
449
|
-
message: `Error: ${errorMessage}`,
|
|
450
|
-
tickets: [],
|
|
249
|
+
export async function* planCoreStream(options) {
|
|
250
|
+
const setup = createPlanAgentConfig(options);
|
|
251
|
+
if ('error' in setup) {
|
|
252
|
+
yield {
|
|
253
|
+
type: 'done',
|
|
254
|
+
data: {
|
|
255
|
+
status: 'error',
|
|
256
|
+
ticketsFile: null,
|
|
257
|
+
tokensUsed: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
258
|
+
cost: 0,
|
|
259
|
+
error: setup.error,
|
|
260
|
+
},
|
|
451
261
|
};
|
|
262
|
+
return;
|
|
452
263
|
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (outputTokens > 0)
|
|
462
|
-
breakdown.push(`${outputTokens.toLocaleString()} output`);
|
|
463
|
-
if (cacheCreationTokens > 0)
|
|
464
|
-
breakdown.push(`${cacheCreationTokens.toLocaleString()} cache write`);
|
|
465
|
-
if (cacheReadTokens > 0)
|
|
466
|
-
breakdown.push(`${cacheReadTokens.toLocaleString()} cache read`);
|
|
467
|
-
return `💰 Cost: $${cost.toFixed(4)} (${breakdown.join(' + ')} tokens)`;
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Process a single Claude interaction with streaming
|
|
471
|
-
* Handles multiple tool calls in a loop until Claude stops calling tools
|
|
472
|
-
*/
|
|
473
|
-
async function processClaudeInteraction(messages, systemPrompt, cwd) {
|
|
474
|
-
const anthropic = new Anthropic({
|
|
475
|
-
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
476
|
-
});
|
|
477
|
-
let responseText = '';
|
|
478
|
-
let tickets = [];
|
|
479
|
-
let ticketsCreated = false;
|
|
480
|
-
let totalInputTokens = 0;
|
|
481
|
-
let totalOutputTokens = 0;
|
|
482
|
-
let totalCacheCreationTokens = 0;
|
|
483
|
-
let totalCacheReadTokens = 0;
|
|
484
|
-
let isFirstOutput = true;
|
|
485
|
-
// Loop until Claude stops calling tools
|
|
486
|
-
const maxIterations = 20; // Safety limit
|
|
487
|
-
let iterations = 0;
|
|
488
|
-
while (iterations < maxIterations) {
|
|
489
|
-
iterations++;
|
|
490
|
-
// Stream the response
|
|
491
|
-
const stream = await anthropic.messages.stream({
|
|
492
|
-
model: 'claude-sonnet-4-20250514',
|
|
493
|
-
max_tokens: 8096,
|
|
494
|
-
system: systemPrompt,
|
|
495
|
-
tools: PLAN_TOOLS,
|
|
496
|
-
messages,
|
|
497
|
-
});
|
|
498
|
-
let currentText = '';
|
|
499
|
-
const toolUses = [];
|
|
500
|
-
// Process stream events
|
|
264
|
+
const { config, ticketsPath, cwd, prompt } = setup;
|
|
265
|
+
try {
|
|
266
|
+
const stream = runAgentStream(prompt, config);
|
|
267
|
+
let sessionId;
|
|
268
|
+
let fullResponse = '';
|
|
269
|
+
let tokensUsed = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
270
|
+
let cost = 0;
|
|
271
|
+
// Yield events from stream
|
|
501
272
|
for await (const event of stream) {
|
|
502
|
-
if (event.type === '
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
273
|
+
if (event.type === 'message') {
|
|
274
|
+
const claudeMessage = event.data;
|
|
275
|
+
yield { type: 'message', data: claudeMessage };
|
|
276
|
+
// Track session ID
|
|
277
|
+
if (claudeMessage.session_id) {
|
|
278
|
+
sessionId = claudeMessage.session_id;
|
|
279
|
+
}
|
|
280
|
+
// Accumulate response text and extract tool calls from assistant messages
|
|
281
|
+
if (claudeMessage.type === 'assistant' && claudeMessage.message) {
|
|
282
|
+
const msg = claudeMessage.message;
|
|
283
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
284
|
+
for (const block of msg.content) {
|
|
285
|
+
if (block.type === 'text' && block.text) {
|
|
286
|
+
fullResponse += block.text;
|
|
287
|
+
}
|
|
288
|
+
else if (block.type === 'tool_use' && block.name) {
|
|
289
|
+
// Yield tool call event
|
|
290
|
+
yield {
|
|
291
|
+
type: 'tool_call',
|
|
292
|
+
data: { name: block.name, input: block.input || {} },
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
507
296
|
}
|
|
508
297
|
}
|
|
509
298
|
}
|
|
510
|
-
else if (event.type === '
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
299
|
+
else if (event.type === 'agent_result') {
|
|
300
|
+
// Use metrics from final result
|
|
301
|
+
tokensUsed = event.data.tokensUsed;
|
|
302
|
+
cost = event.data.cost;
|
|
303
|
+
if (event.data.sessionId) {
|
|
304
|
+
sessionId = event.data.sessionId;
|
|
515
305
|
}
|
|
516
306
|
}
|
|
517
307
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
// If no tools called, we're done
|
|
538
|
-
if (toolUses.length === 0) {
|
|
539
|
-
messages = [...messages, { role: 'assistant', content: finalMessage.content }];
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
// Execute tools
|
|
543
|
-
messages = [...messages, { role: 'assistant', content: finalMessage.content }];
|
|
544
|
-
const toolResults = [];
|
|
545
|
-
for (const tool of toolUses) {
|
|
546
|
-
if (tool.name === 'read_file') {
|
|
547
|
-
const result = executeReadFile(tool.input, cwd);
|
|
548
|
-
toolResults.push({
|
|
549
|
-
type: 'tool_result',
|
|
550
|
-
tool_use_id: tool.id,
|
|
551
|
-
content: result.content,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
else if (tool.name === 'list_directory') {
|
|
555
|
-
const result = executeListDirectory(tool.input, cwd);
|
|
556
|
-
toolResults.push({
|
|
557
|
-
type: 'tool_result',
|
|
558
|
-
tool_use_id: tool.id,
|
|
559
|
-
content: result.content,
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
else if (tool.name === 'glob_search') {
|
|
563
|
-
const result = await executeGlobSearch(tool.input, cwd);
|
|
564
|
-
toolResults.push({
|
|
565
|
-
type: 'tool_result',
|
|
566
|
-
tool_use_id: tool.id,
|
|
567
|
-
content: result.content,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
else if (tool.name === 'write_tickets') {
|
|
571
|
-
const result = executeWriteTickets(tool.input);
|
|
572
|
-
tickets = result.tickets;
|
|
573
|
-
ticketsCreated = result.success;
|
|
574
|
-
toolResults.push({
|
|
575
|
-
type: 'tool_result',
|
|
576
|
-
tool_use_id: tool.id,
|
|
577
|
-
content: result.message,
|
|
578
|
-
});
|
|
308
|
+
// Try to parse tickets from response (Phase 2)
|
|
309
|
+
try {
|
|
310
|
+
const tickets = parseTickets(fullResponse);
|
|
311
|
+
// If no tickets were parsed, this is Phase 1 (clarification questions)
|
|
312
|
+
if (tickets.length === 0) {
|
|
313
|
+
yield {
|
|
314
|
+
type: 'done',
|
|
315
|
+
data: {
|
|
316
|
+
status: 'input_required',
|
|
317
|
+
ticketsFile: null,
|
|
318
|
+
tokensUsed,
|
|
319
|
+
cost,
|
|
320
|
+
sessionId,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
return;
|
|
579
324
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const followupStream = await anthropic.messages.stream({
|
|
585
|
-
model: 'claude-sonnet-4-20250514',
|
|
586
|
-
max_tokens: 8096,
|
|
587
|
-
system: systemPrompt,
|
|
588
|
-
tools: PLAN_TOOLS,
|
|
589
|
-
messages,
|
|
325
|
+
// Use full pipeline: validate, sort, write
|
|
326
|
+
await processAndWriteTickets(tickets, ticketsPath, cwd, {
|
|
327
|
+
skipValidation: false,
|
|
328
|
+
displaySummary: false, // Don't display in stream mode
|
|
590
329
|
});
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
const followupMessage = await followupStream.finalMessage();
|
|
602
|
-
responseText += '\n' + followupText;
|
|
603
|
-
messages = [...messages, { role: 'assistant', content: followupMessage.content }];
|
|
604
|
-
// Add followup token usage
|
|
605
|
-
const followupUsage = followupMessage.usage;
|
|
606
|
-
totalInputTokens += followupUsage.input_tokens;
|
|
607
|
-
totalOutputTokens += followupUsage.output_tokens;
|
|
608
|
-
totalCacheCreationTokens += followupUsage.cache_creation_input_tokens || 0;
|
|
609
|
-
totalCacheReadTokens += followupUsage.cache_read_input_tokens || 0;
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return {
|
|
614
|
-
response: responseText,
|
|
615
|
-
messages,
|
|
616
|
-
inputTokens: totalInputTokens,
|
|
617
|
-
outputTokens: totalOutputTokens,
|
|
618
|
-
cacheCreationTokens: totalCacheCreationTokens,
|
|
619
|
-
cacheReadTokens: totalCacheReadTokens,
|
|
620
|
-
tickets,
|
|
621
|
-
ticketsCreated,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Interactive planning session
|
|
626
|
-
*/
|
|
627
|
-
async function interactivePlanSession(initialPrompt, cwd, ticketsPath, logContext, noTest = false) {
|
|
628
|
-
console.log(`
|
|
629
|
-
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
630
|
-
║ Kosuke Plan - AI-Driven Ticket Planning ║
|
|
631
|
-
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
632
|
-
`);
|
|
633
|
-
console.log('💡 This tool will help you create implementation tickets from your feature/bug description.\n');
|
|
634
|
-
console.log('🤖 Claude will explore your codebase to understand patterns and conventions.\n');
|
|
635
|
-
// Read CLAUDE.md and inject into system prompt
|
|
636
|
-
const claudeMdContent = readClaudeMd(cwd);
|
|
637
|
-
if (claudeMdContent) {
|
|
638
|
-
console.log(`📖 Loaded CLAUDE.md (${Math.round(claudeMdContent.length / 1000)}k chars)\n`);
|
|
639
|
-
}
|
|
640
|
-
const systemPrompt = buildPlanSystemPrompt(claudeMdContent, noTest);
|
|
641
|
-
console.log(`${'─'.repeat(60)}`);
|
|
642
|
-
console.log('🤖 Using model: claude-sonnet-4-5');
|
|
643
|
-
console.log(`${'─'.repeat(60)}\n`);
|
|
644
|
-
console.log('✨ Tip: Enter to submit, Ctrl+J for new lines.\n');
|
|
645
|
-
// Set up Ctrl+C handler
|
|
646
|
-
const handleSigInt = async () => {
|
|
647
|
-
console.log('\n\n👋 Exiting planning session...\n');
|
|
648
|
-
if (logContext) {
|
|
649
|
-
await logger.complete(logContext, 'cancelled');
|
|
330
|
+
yield {
|
|
331
|
+
type: 'done',
|
|
332
|
+
data: {
|
|
333
|
+
status: 'success',
|
|
334
|
+
ticketsFile: ticketsPath,
|
|
335
|
+
tokensUsed,
|
|
336
|
+
cost,
|
|
337
|
+
sessionId,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
650
340
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
session.messages.push({ role: 'user', content: initialPrompt });
|
|
667
|
-
let continueConversation = true;
|
|
668
|
-
while (continueConversation) {
|
|
669
|
-
console.log('\n🤔 Claude is analyzing...\n');
|
|
670
|
-
const result = await processClaudeInteraction(session.messages, systemPrompt, cwd);
|
|
671
|
-
session.messages = result.messages;
|
|
672
|
-
// Track costs
|
|
673
|
-
totalInputTokens += result.inputTokens;
|
|
674
|
-
totalOutputTokens += result.outputTokens;
|
|
675
|
-
totalCacheCreationTokens += result.cacheCreationTokens;
|
|
676
|
-
totalCacheReadTokens += result.cacheReadTokens;
|
|
677
|
-
const batchCost = calculateCost(result.inputTokens, result.outputTokens, result.cacheCreationTokens, result.cacheReadTokens);
|
|
678
|
-
totalCost += batchCost;
|
|
679
|
-
// Display cost
|
|
680
|
-
console.log('\n' + '─'.repeat(90));
|
|
681
|
-
console.log(formatTokenUsage(result.inputTokens, result.outputTokens, result.cacheCreationTokens, result.cacheReadTokens, batchCost));
|
|
682
|
-
console.log('─'.repeat(90) + '\n');
|
|
683
|
-
// Check if tickets were created
|
|
684
|
-
if (result.ticketsCreated) {
|
|
685
|
-
// Validate and write tickets using shared utility
|
|
686
|
-
const { tickets: validatedTickets } = await processAndWriteTickets(result.tickets, ticketsPath, cwd, { displaySummary: true });
|
|
687
|
-
finalTickets = validatedTickets;
|
|
688
|
-
console.log('═'.repeat(90));
|
|
689
|
-
console.log('📊 Total Session Cost:');
|
|
690
|
-
console.log(formatTokenUsage(totalInputTokens, totalOutputTokens, totalCacheCreationTokens, totalCacheReadTokens, totalCost));
|
|
691
|
-
console.log('═'.repeat(90));
|
|
692
|
-
// Get relative path for cleaner output
|
|
693
|
-
const relativeTicketsPath = ticketsPath.replace(cwd + '/', '');
|
|
694
|
-
console.log('\n🎉 Planning complete!\n');
|
|
695
|
-
console.log('💡 Next steps:');
|
|
696
|
-
console.log(' - Review tickets: cat "' + ticketsPath + '"');
|
|
697
|
-
console.log(' - Build tickets: kosuke build --directory="' +
|
|
698
|
-
cwd +
|
|
699
|
-
'" --tickets="' +
|
|
700
|
-
relativeTicketsPath +
|
|
701
|
-
'"');
|
|
702
|
-
console.log(' - List all tickets: ls ' + join(cwd, 'tickets'));
|
|
703
|
-
continueConversation = false;
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
706
|
-
// Ask for user response
|
|
707
|
-
console.log('💬 Your response (type "exit" to quit):\n');
|
|
708
|
-
const userResponse = await askQuestion('You: ');
|
|
709
|
-
if (!userResponse) {
|
|
710
|
-
console.log('\n⚠️ Empty response. Please provide an answer or type "exit".');
|
|
711
|
-
continue;
|
|
341
|
+
catch (parseError) {
|
|
342
|
+
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
343
|
+
// Check if this is a parse/validation error or just Phase 1
|
|
344
|
+
if (errorMsg.includes('No JSON array found')) {
|
|
345
|
+
// This is Phase 1 (clarification questions)
|
|
346
|
+
yield {
|
|
347
|
+
type: 'done',
|
|
348
|
+
data: {
|
|
349
|
+
status: 'input_required',
|
|
350
|
+
ticketsFile: null,
|
|
351
|
+
tokensUsed,
|
|
352
|
+
cost,
|
|
353
|
+
sessionId,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
712
356
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
357
|
+
else {
|
|
358
|
+
// Actual validation error
|
|
359
|
+
yield {
|
|
360
|
+
type: 'done',
|
|
361
|
+
data: {
|
|
362
|
+
status: 'error',
|
|
363
|
+
ticketsFile: null,
|
|
364
|
+
tokensUsed,
|
|
365
|
+
cost: 0,
|
|
366
|
+
error: errorMsg,
|
|
367
|
+
},
|
|
368
|
+
};
|
|
721
369
|
}
|
|
722
|
-
// Add user response to messages
|
|
723
|
-
session.messages = [...session.messages, { role: 'user', content: userResponse }];
|
|
724
370
|
}
|
|
725
371
|
}
|
|
726
372
|
catch (error) {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
373
|
+
yield {
|
|
374
|
+
type: 'done',
|
|
375
|
+
data: {
|
|
376
|
+
status: 'error',
|
|
377
|
+
ticketsFile: null,
|
|
378
|
+
tokensUsed: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
379
|
+
cost: 0,
|
|
380
|
+
error: error instanceof Error ? error.message : String(error),
|
|
381
|
+
},
|
|
382
|
+
};
|
|
732
383
|
}
|
|
733
|
-
return {
|
|
734
|
-
messages: session.messages,
|
|
735
|
-
tickets: finalTickets,
|
|
736
|
-
tokensUsed: {
|
|
737
|
-
input: totalInputTokens,
|
|
738
|
-
output: totalOutputTokens,
|
|
739
|
-
cacheCreation: totalCacheCreationTokens,
|
|
740
|
-
cacheRead: totalCacheReadTokens,
|
|
741
|
-
},
|
|
742
|
-
cost: totalCost,
|
|
743
|
-
};
|
|
744
384
|
}
|
|
745
385
|
/**
|
|
746
|
-
*
|
|
386
|
+
* Plan interactive session for CLI (single turn with logging)
|
|
747
387
|
*/
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
const cwd = directory ? resolve(directory) : process.cwd();
|
|
752
|
-
if (!existsSync(cwd)) {
|
|
753
|
-
return {
|
|
754
|
-
success: false,
|
|
755
|
-
tickets: [],
|
|
756
|
-
ticketsFile: '',
|
|
757
|
-
tokensUsed: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
758
|
-
cost: 0,
|
|
759
|
-
error: `Directory not found: ${cwd}`,
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
const stats = statSync(cwd);
|
|
763
|
-
if (!stats.isDirectory()) {
|
|
388
|
+
async function planInteractiveSession(options) {
|
|
389
|
+
const setup = createPlanAgentConfig(options);
|
|
390
|
+
if ('error' in setup) {
|
|
764
391
|
return {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
ticketsFile: '',
|
|
392
|
+
status: 'error',
|
|
393
|
+
ticketsFile: null,
|
|
768
394
|
tokensUsed: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
769
395
|
cost: 0,
|
|
770
|
-
error:
|
|
396
|
+
error: setup.error,
|
|
771
397
|
};
|
|
772
398
|
}
|
|
773
|
-
const ticketsPath =
|
|
399
|
+
const { config, ticketsPath, cwd, prompt } = setup;
|
|
774
400
|
try {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
401
|
+
console.log('\n🤔 Claude is analyzing...\n');
|
|
402
|
+
// Run agent with logging
|
|
403
|
+
const result = await runAgent(prompt, config);
|
|
404
|
+
// Display cost
|
|
405
|
+
console.log('\n' + '─'.repeat(90));
|
|
406
|
+
console.log(`💰 Cost: ${formatCostBreakdown(result)}`);
|
|
407
|
+
console.log('─'.repeat(90) + '\n');
|
|
408
|
+
// Try to parse tickets from response (Phase 2)
|
|
409
|
+
try {
|
|
410
|
+
const tickets = parseTickets(result.response);
|
|
411
|
+
// If no tickets were parsed, this is Phase 1 (clarification questions)
|
|
412
|
+
if (tickets.length === 0) {
|
|
413
|
+
return {
|
|
414
|
+
status: 'input_required',
|
|
415
|
+
ticketsFile: null,
|
|
416
|
+
tokensUsed: result.tokensUsed,
|
|
417
|
+
cost: result.cost,
|
|
418
|
+
sessionId: result.sessionId,
|
|
419
|
+
message: result.response,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// Use full pipeline: validate, sort, write
|
|
423
|
+
await processAndWriteTickets(tickets, ticketsPath, cwd, {
|
|
424
|
+
skipValidation: false,
|
|
425
|
+
displaySummary: true, // Display summary in interactive mode
|
|
426
|
+
});
|
|
427
|
+
console.log(`\n✅ Successfully created ${tickets.length} tickets: ${ticketsPath}\n`);
|
|
428
|
+
return {
|
|
429
|
+
status: 'success',
|
|
430
|
+
ticketsFile: ticketsPath,
|
|
431
|
+
tokensUsed: result.tokensUsed,
|
|
432
|
+
cost: result.cost,
|
|
433
|
+
sessionId: result.sessionId,
|
|
434
|
+
message: result.response,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
catch (parseError) {
|
|
438
|
+
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
439
|
+
// Check if this is a parse/validation error or just Phase 1
|
|
440
|
+
if (errorMsg.includes('No JSON array found')) {
|
|
441
|
+
// This is Phase 1 (clarification questions)
|
|
442
|
+
return {
|
|
443
|
+
status: 'input_required',
|
|
444
|
+
ticketsFile: null,
|
|
445
|
+
tokensUsed: result.tokensUsed,
|
|
446
|
+
cost: result.cost,
|
|
447
|
+
sessionId: result.sessionId,
|
|
448
|
+
message: result.response,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
// Actual validation error - show it
|
|
452
|
+
console.error(`\n❌ Ticket validation failed: ${errorMsg}\n`);
|
|
453
|
+
return {
|
|
454
|
+
status: 'error',
|
|
455
|
+
ticketsFile: null,
|
|
456
|
+
tokensUsed: result.tokensUsed,
|
|
457
|
+
cost: result.cost,
|
|
458
|
+
error: errorMsg,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
783
461
|
}
|
|
784
462
|
catch (error) {
|
|
785
463
|
return {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
ticketsFile: '',
|
|
464
|
+
status: 'error',
|
|
465
|
+
ticketsFile: null,
|
|
789
466
|
tokensUsed: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
790
467
|
cost: 0,
|
|
791
468
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -793,7 +470,7 @@ export async function planCore(options) {
|
|
|
793
470
|
}
|
|
794
471
|
}
|
|
795
472
|
/**
|
|
796
|
-
* Main plan command
|
|
473
|
+
* Main plan command (interactive CLI wrapper)
|
|
797
474
|
*/
|
|
798
475
|
export async function planCommand(options) {
|
|
799
476
|
// Initialize logging
|
|
@@ -818,14 +495,129 @@ export async function planCommand(options) {
|
|
|
818
495
|
if (!stats.isDirectory()) {
|
|
819
496
|
throw new Error(`Path is not a directory: ${cwd}`);
|
|
820
497
|
}
|
|
498
|
+
console.log(`
|
|
499
|
+
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
500
|
+
║ Kosuke Plan - AI-Driven Ticket Planning ║
|
|
501
|
+
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
502
|
+
`);
|
|
503
|
+
console.log('💡 This tool will help you create implementation tickets from your feature/bug description.\n');
|
|
504
|
+
console.log('🤖 Claude will explore your codebase to understand patterns and conventions.\n');
|
|
505
|
+
console.log('✨ Tip: Enter to submit, Ctrl+J for new lines.\n');
|
|
821
506
|
console.log(`📁 Using project directory: ${cwd}\n`);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
507
|
+
// Set up Ctrl+C handler
|
|
508
|
+
const handleSigInt = async () => {
|
|
509
|
+
console.log('\n\n👋 Exiting planning session...\n');
|
|
510
|
+
await logger.complete(logContext, 'cancelled');
|
|
511
|
+
cleanupHandler();
|
|
512
|
+
process.exit(0);
|
|
513
|
+
};
|
|
514
|
+
process.on('SIGINT', handleSigInt);
|
|
515
|
+
let currentPrompt = options.prompt;
|
|
516
|
+
let sessionId = options.resume;
|
|
517
|
+
let totalInputTokens = 0;
|
|
518
|
+
let totalOutputTokens = 0;
|
|
519
|
+
let totalCacheCreationTokens = 0;
|
|
520
|
+
let totalCacheReadTokens = 0;
|
|
521
|
+
let totalCost = 0;
|
|
522
|
+
try {
|
|
523
|
+
// Interactive loop
|
|
524
|
+
while (true) {
|
|
525
|
+
const result = await planInteractiveSession({
|
|
526
|
+
prompt: currentPrompt,
|
|
527
|
+
directory: cwd,
|
|
528
|
+
noTest: options.noTest,
|
|
529
|
+
resume: sessionId,
|
|
530
|
+
});
|
|
531
|
+
// Track cumulative costs
|
|
532
|
+
totalInputTokens += result.tokensUsed.input;
|
|
533
|
+
totalOutputTokens += result.tokensUsed.output;
|
|
534
|
+
totalCacheCreationTokens += result.tokensUsed.cacheCreation;
|
|
535
|
+
totalCacheReadTokens += result.tokensUsed.cacheRead;
|
|
536
|
+
totalCost += result.cost;
|
|
537
|
+
sessionId = result.sessionId;
|
|
538
|
+
if (result.status === 'success') {
|
|
539
|
+
// Planning complete
|
|
540
|
+
console.log('═'.repeat(90));
|
|
541
|
+
console.log('📊 Total Session Cost:');
|
|
542
|
+
console.log(`💰 ${formatCostBreakdown({
|
|
543
|
+
tokensUsed: {
|
|
544
|
+
input: totalInputTokens,
|
|
545
|
+
output: totalOutputTokens,
|
|
546
|
+
cacheCreation: totalCacheCreationTokens,
|
|
547
|
+
cacheRead: totalCacheReadTokens,
|
|
548
|
+
},
|
|
549
|
+
cost: totalCost,
|
|
550
|
+
response: '',
|
|
551
|
+
fixCount: 0,
|
|
552
|
+
filesReferenced: new Set(),
|
|
553
|
+
})}`);
|
|
554
|
+
console.log('═'.repeat(90));
|
|
555
|
+
const relativeTicketsPath = result.ticketsFile.replace(cwd + '/', '');
|
|
556
|
+
console.log('\n🎉 Planning complete!\n');
|
|
557
|
+
console.log('💡 Next steps:');
|
|
558
|
+
console.log(' - Review tickets: cat "' + result.ticketsFile + '"');
|
|
559
|
+
console.log(' - Build tickets: kosuke build --directory="' +
|
|
560
|
+
cwd +
|
|
561
|
+
'" --tickets="' +
|
|
562
|
+
relativeTicketsPath +
|
|
563
|
+
'"');
|
|
564
|
+
console.log(' - List all tickets: ls ' + join(cwd, 'tickets'));
|
|
565
|
+
if (sessionId) {
|
|
566
|
+
console.log(`\n💾 Session ID: ${sessionId}`);
|
|
567
|
+
}
|
|
568
|
+
logger.trackTokens(logContext, {
|
|
569
|
+
input: totalInputTokens,
|
|
570
|
+
output: totalOutputTokens,
|
|
571
|
+
cacheCreation: totalCacheCreationTokens,
|
|
572
|
+
cacheRead: totalCacheReadTokens,
|
|
573
|
+
});
|
|
574
|
+
await logger.complete(logContext, 'success');
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
else if (result.status === 'input_required') {
|
|
578
|
+
// Ask for user response
|
|
579
|
+
console.log('💬 Your response (type "exit" to quit):\n');
|
|
580
|
+
const userResponse = await askQuestion('You: ');
|
|
581
|
+
if (!userResponse) {
|
|
582
|
+
console.log('\n⚠️ Empty response. Please provide an answer or type "exit".');
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (userResponse.toLowerCase() === 'exit') {
|
|
586
|
+
console.log('\n👋 Exiting planning session.\n');
|
|
587
|
+
if (sessionId) {
|
|
588
|
+
console.log('💾 Session ID (to resume later):');
|
|
589
|
+
console.log(` ${sessionId}\n`);
|
|
590
|
+
console.log(' Resume with: kosuke plan --prompt="continue" --resume=' + sessionId + '\n');
|
|
591
|
+
}
|
|
592
|
+
console.log('═'.repeat(90));
|
|
593
|
+
console.log('📊 Session Cost:');
|
|
594
|
+
console.log(`💰 ${formatCostBreakdown({
|
|
595
|
+
tokensUsed: {
|
|
596
|
+
input: totalInputTokens,
|
|
597
|
+
output: totalOutputTokens,
|
|
598
|
+
cacheCreation: totalCacheCreationTokens,
|
|
599
|
+
cacheRead: totalCacheReadTokens,
|
|
600
|
+
},
|
|
601
|
+
cost: totalCost,
|
|
602
|
+
response: '',
|
|
603
|
+
fixCount: 0,
|
|
604
|
+
filesReferenced: new Set(),
|
|
605
|
+
})}`);
|
|
606
|
+
console.log('═'.repeat(90) + '\n');
|
|
607
|
+
await logger.complete(logContext, 'cancelled');
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
currentPrompt = userResponse;
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// Error
|
|
614
|
+
throw new Error(result.error || 'Unknown error during planning');
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
process.removeListener('SIGINT', handleSigInt);
|
|
620
|
+
}
|
|
829
621
|
cleanupHandler();
|
|
830
622
|
}
|
|
831
623
|
catch (error) {
|