@kosuke-ai/cli 0.0.44 → 0.0.45

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.
Files changed (82) hide show
  1. package/README.md +94 -1
  2. package/claude.costs.json +34 -0
  3. package/dist/index.js +27 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/kosuke/commands/build.d.ts +5 -1
  6. package/dist/kosuke/commands/build.d.ts.map +1 -1
  7. package/dist/kosuke/commands/build.js +297 -3
  8. package/dist/kosuke/commands/build.js.map +1 -1
  9. package/dist/kosuke/commands/migrate.d.ts +6 -1
  10. package/dist/kosuke/commands/migrate.d.ts.map +1 -1
  11. package/dist/kosuke/commands/migrate.js +124 -37
  12. package/dist/kosuke/commands/migrate.js.map +1 -1
  13. package/dist/kosuke/commands/plan.d.ts +4 -20
  14. package/dist/kosuke/commands/plan.d.ts.map +1 -1
  15. package/dist/kosuke/commands/plan.js +471 -679
  16. package/dist/kosuke/commands/plan.js.map +1 -1
  17. package/dist/kosuke/commands/requirements.js +2 -2
  18. package/dist/kosuke/commands/requirements.js.map +1 -1
  19. package/dist/kosuke/commands/review.d.ts +6 -1
  20. package/dist/kosuke/commands/review.d.ts.map +1 -1
  21. package/dist/kosuke/commands/review.js +168 -68
  22. package/dist/kosuke/commands/review.js.map +1 -1
  23. package/dist/kosuke/commands/serve.d.ts +9 -0
  24. package/dist/kosuke/commands/serve.d.ts.map +1 -0
  25. package/dist/kosuke/commands/serve.js +17 -0
  26. package/dist/kosuke/commands/serve.js.map +1 -0
  27. package/dist/kosuke/commands/ship.d.ts +6 -1
  28. package/dist/kosuke/commands/ship.d.ts.map +1 -1
  29. package/dist/kosuke/commands/ship.js +226 -67
  30. package/dist/kosuke/commands/ship.js.map +1 -1
  31. package/dist/kosuke/commands/test.d.ts +6 -1
  32. package/dist/kosuke/commands/test.d.ts.map +1 -1
  33. package/dist/kosuke/commands/test.js +171 -73
  34. package/dist/kosuke/commands/test.js.map +1 -1
  35. package/dist/kosuke/commands/tickets.d.ts +7 -15
  36. package/dist/kosuke/commands/tickets.d.ts.map +1 -1
  37. package/dist/kosuke/commands/tickets.js +14 -58
  38. package/dist/kosuke/commands/tickets.js.map +1 -1
  39. package/dist/kosuke/serve/middleware/error-handler.d.ts +13 -0
  40. package/dist/kosuke/serve/middleware/error-handler.d.ts.map +1 -0
  41. package/dist/kosuke/serve/middleware/error-handler.js +34 -0
  42. package/dist/kosuke/serve/middleware/error-handler.js.map +1 -0
  43. package/dist/kosuke/serve/middleware/validation.d.ts +13 -0
  44. package/dist/kosuke/serve/middleware/validation.d.ts.map +1 -0
  45. package/dist/kosuke/serve/middleware/validation.js +39 -0
  46. package/dist/kosuke/serve/middleware/validation.js.map +1 -0
  47. package/dist/kosuke/serve/routes/build.d.ts +6 -0
  48. package/dist/kosuke/serve/routes/build.d.ts.map +1 -0
  49. package/dist/kosuke/serve/routes/build.js +224 -0
  50. package/dist/kosuke/serve/routes/build.js.map +1 -0
  51. package/dist/kosuke/serve/routes/health.d.ts +6 -0
  52. package/dist/kosuke/serve/routes/health.d.ts.map +1 -0
  53. package/dist/kosuke/serve/routes/health.js +11 -0
  54. package/dist/kosuke/serve/routes/health.js.map +1 -0
  55. package/dist/kosuke/serve/routes/plan.d.ts +6 -0
  56. package/dist/kosuke/serve/routes/plan.d.ts.map +1 -0
  57. package/dist/kosuke/serve/routes/plan.js +104 -0
  58. package/dist/kosuke/serve/routes/plan.js.map +1 -0
  59. package/dist/kosuke/serve/server.d.ts +16 -0
  60. package/dist/kosuke/serve/server.d.ts.map +1 -0
  61. package/dist/kosuke/serve/server.js +63 -0
  62. package/dist/kosuke/serve/server.js.map +1 -0
  63. package/dist/kosuke/serve/validation/build.d.ts +41 -0
  64. package/dist/kosuke/serve/validation/build.d.ts.map +1 -0
  65. package/dist/kosuke/serve/validation/build.js +20 -0
  66. package/dist/kosuke/serve/validation/build.js.map +1 -0
  67. package/dist/kosuke/serve/validation/plan.d.ts +26 -0
  68. package/dist/kosuke/serve/validation/plan.d.ts.map +1 -0
  69. package/dist/kosuke/serve/validation/plan.js +15 -0
  70. package/dist/kosuke/serve/validation/plan.js.map +1 -0
  71. package/dist/kosuke/types.d.ts +205 -0
  72. package/dist/kosuke/types.d.ts.map +1 -1
  73. package/dist/kosuke/utils/claude-agent.d.ts +82 -6
  74. package/dist/kosuke/utils/claude-agent.d.ts.map +1 -1
  75. package/dist/kosuke/utils/claude-agent.js +381 -180
  76. package/dist/kosuke/utils/claude-agent.js.map +1 -1
  77. package/dist/lib.d.ts +4 -4
  78. package/dist/lib.d.ts.map +1 -1
  79. package/dist/lib.js +2 -1
  80. package/dist/lib.js.map +1 -1
  81. package/dist/package.json +6 -2
  82. 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 Anthropic from '@anthropic-ai/sdk';
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 { glob } from 'glob';
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 { processAndWriteTickets, sortTicketsByOrder } from '../utils/tickets-manager.js';
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}.ticket.json`);
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
- * @param claudeMdContent - Content of CLAUDE.md file if it exists
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(claudeMdContent, noTest = false) {
158
- const claudeSection = claudeMdContent
159
- ? `
160
-
161
- **PROJECT CONTEXT (from CLAUDE.md):**
52
+ function buildPlanSystemPrompt(noTest = false) {
53
+ const testGuidelines = noTest
54
+ ? ''
55
+ : `
162
56
 
163
- ${claudeMdContent}
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. **Explore Codebase**:
175
- - Use list_directory to explore relevant parts of the codebase
176
- - Read existing similar implementations to understand patterns
177
- - Analyze what the user wants to achieve
178
- - Identify what's unclear or needs user input
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
- 2. **Ask Clarification Questions**: Present questions in this format:
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 question, provide BOTH the question AND a recommended approach:
88
+ For each clarification needed, provide BOTH a question AND a recommended approach:
190
89
 
191
90
  1. **[Topic]**
192
- - Question: [User-focused question - NOT technical]
193
- - 💡 Recommendation: [Simple, practical default choice]
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
- 2. **[Topic]**
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:** Reply "go with recommendations" to accept all defaults.
100
+ **Quick Response Option:** You can reply "go for recommendations" to accept all recommended approaches at once.
200
101
  ---
201
102
 
202
- 3. **Iterative Refinement**: As the user answers:
203
- - If user says "go with recommendations", accept all defaults
204
- - If user provides specific answers, incorporate them
205
- - Ask follow-up questions ONLY if critical information is still missing
206
- - Bias towards simplicity - this is an MVP
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
- 4. **Generate Tickets**: Once requirements are clear, use \`write_tickets\` tool to create tickets:
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
- **Ticket Types & Prefixes:**
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
- **When to use ENGINE vs BACKEND:**
217
- - **Use BACKEND (Next.js)** for: CRUD operations, auth logic, business rules, anything TypeScript handles well (90% of features)
218
- - **Use ENGINE (Python)** for: ML/AI, data science (numpy/pandas), complex algorithms, PDF/document parsing, image processing, or when Python libraries are required${noTest
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
- **Ticket Order (build system processes in this order):**
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
- **Examples:**
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
- **Ticket Generation Rules:**
280
- - Generate only the tickets actually needed
281
- - Ensure tickets are atomic and independently implementable
282
- - Include clear acceptance criteria in each ticket description
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
- **Example Tickets (Full JSON):**
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": "Create tasks tRPC router",
297
- "description": "Create backend API for tasks:\\n- Create lib/trpc/routers/tasks.ts\\n- Implement CRUD operations (list, create, update, delete)\\n- Server-side filtering by status\\n\\n**Acceptance Criteria:**\\n- All CRUD operations work\\n- Authorization enforced\\n- Type-safe implementation",
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
- "category": "tasks"
141
+ "status": "Todo",
142
+ "category": "orders"
301
143
  },
302
144
  {
303
145
  "id": "PLAN-FRONTEND-1",
304
- "title": "Create tasks page with list and filters",
305
- "description": "Create tasks management UI:\\n- Create app/(logged-in)/tasks/page.tsx\\n- Task list with status filters\\n- Add new task dialog\\n- Edit/delete actions\\n\\n**Acceptance Criteria:**\\n- Task list displays correctly\\n- Filters work\\n- CRUD operations functional\\n- Responsive design\\n\\n**Technical Notes:**\\n- Use existing UI components from components/ui/\\n- Follow page patterns from existing routes",
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": 6,
308
- "category": "tasks"
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: User creates and manages tasks",
313
- "description": "**Test User Credentials:**\\n- Email: john+kosuke_test@example.com\\n- OTP Code: 424242\\n\\n**Test Steps:**\\n\\n1. **Sign in**\\n - Navigate to /sign-in\\n - Enter email: john+kosuke_test@example.com\\n - Click 'Send Code' button\\n - Enter OTP: 424242\\n - Click 'Verify'\\n - Expected: Redirected to /tasks\\n\\n2. **Create task**\\n - Click 'New Task' button\\n - Enter title: 'Test Task'\\n - Click 'Create'\\n - Expected: Task appears in list\\n\\n3. **Delete task**\\n - Click delete button on task\\n - Confirm deletion\\n - Expected: Task removed\\n\\n**Acceptance Criteria:**\\n- User authenticates successfully\\n- Task CRUD operations work\\n- UI provides feedback",
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": 4,
316
- "category": "tasks"
317
- }
318
- ]`;
319
- }
320
- /**
321
- * Execute read_file tool
322
- */
323
- function executeReadFile(toolInput, cwd) {
324
- try {
325
- const filePath = toolInput.path;
326
- const fullPath = join(cwd, filePath);
327
- if (!existsSync(fullPath)) {
328
- return { success: false, content: `File not found: ${filePath}` };
329
- }
330
- const stats = statSync(fullPath);
331
- if (stats.isDirectory()) {
332
- return { success: false, content: `Path is a directory, not a file: ${filePath}` };
333
- }
334
- const content = readFileSync(fullPath, 'utf-8');
335
- console.log(`\n 📖 Reading: ${filePath}`);
336
- return { success: true, content };
337
- }
338
- catch (error) {
339
- const msg = error instanceof Error ? error.message : String(error);
340
- return { success: false, content: `Error reading file: ${msg}` };
341
- }
342
- }
343
- /**
344
- * Execute list_directory tool
345
- */
346
- function executeListDirectory(toolInput, cwd) {
347
- try {
348
- const dirPath = toolInput.path || '.';
349
- const fullPath = join(cwd, dirPath);
350
- if (!existsSync(fullPath)) {
351
- return { success: false, content: `Directory not found: ${dirPath}` };
352
- }
353
- const stats = statSync(fullPath);
354
- if (!stats.isDirectory()) {
355
- return { success: false, content: `Path is not a directory: ${dirPath}` };
356
- }
357
- const entries = readdirSync(fullPath);
358
- const items = [];
359
- // Filter out common ignored directories
360
- const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.tmp'];
361
- for (const entry of entries.sort()) {
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
- * Execute glob_search tool
209
+ * Shared setup for plan agent configuration
390
210
  */
391
- async function executeGlobSearch(toolInput, cwd) {
392
- try {
393
- const pattern = toolInput.pattern;
394
- const files = await glob(pattern, {
395
- cwd,
396
- nodir: true,
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
- catch (error) {
414
- const msg = error instanceof Error ? error.message : String(error);
415
- return { success: false, content: `Error searching files: ${msg}` };
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
- * Execute write_tickets tool
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 executeWriteTickets(toolInput) {
423
- try {
424
- const inputTickets = toolInput.tickets;
425
- // Transform to full Ticket objects
426
- const tickets = inputTickets.map((t) => ({
427
- id: t.id,
428
- title: t.title,
429
- description: t.description,
430
- type: t.type,
431
- estimatedEffort: t.estimatedEffort,
432
- status: 'Todo',
433
- category: t.category,
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
- * Format token usage for display
456
- */
457
- function formatTokenUsage(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, cost) {
458
- const breakdown = [];
459
- if (inputTokens > 0)
460
- breakdown.push(`${inputTokens.toLocaleString()} input`);
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 === 'content_block_start') {
503
- if (event.content_block.type === 'text') {
504
- if (isFirstOutput) {
505
- process.stdout.write('\n> Claude:\n');
506
- isFirstOutput = false;
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 === 'content_block_delta') {
511
- if (event.delta.type === 'text_delta') {
512
- const delta = event.delta.text;
513
- currentText += delta;
514
- process.stdout.write(delta);
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
- responseText += currentText;
519
- // Get final message
520
- const finalMessage = await stream.finalMessage();
521
- // Track token usage
522
- const usage = finalMessage.usage;
523
- totalInputTokens += usage.input_tokens;
524
- totalOutputTokens += usage.output_tokens;
525
- totalCacheCreationTokens += usage.cache_creation_input_tokens || 0;
526
- totalCacheReadTokens += usage.cache_read_input_tokens || 0;
527
- // Extract tool uses
528
- for (const block of finalMessage.content) {
529
- if (block.type === 'tool_use') {
530
- toolUses.push({
531
- id: block.id,
532
- name: block.name,
533
- input: block.input,
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
- messages = [...messages, { role: 'user', content: toolResults }];
582
- // If tickets were created, get final response and stop
583
- if (ticketsCreated) {
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
- let followupText = '';
592
- for await (const event of followupStream) {
593
- if (event.type === 'content_block_delta') {
594
- if (event.delta.type === 'text_delta') {
595
- const delta = event.delta.text;
596
- followupText += delta;
597
- process.stdout.write(delta);
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
- process.exit(0);
652
- };
653
- process.on('SIGINT', handleSigInt);
654
- const session = {
655
- prompt: initialPrompt,
656
- messages: [],
657
- };
658
- let totalInputTokens = 0;
659
- let totalOutputTokens = 0;
660
- let totalCacheCreationTokens = 0;
661
- let totalCacheReadTokens = 0;
662
- let totalCost = 0;
663
- let finalTickets = [];
664
- try {
665
- // Start with initial prompt
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
- if (userResponse.toLowerCase() === 'exit') {
714
- console.log('\n👋 Exiting planning session.\n');
715
- console.log('═'.repeat(90));
716
- console.log('📊 Session Cost:');
717
- console.log(formatTokenUsage(totalInputTokens, totalOutputTokens, totalCacheCreationTokens, totalCacheReadTokens, totalCost));
718
- console.log('═'.repeat(90) + '\n');
719
- continueConversation = false;
720
- break;
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
- console.error('\n❌ Error during planning:', error);
728
- throw error;
729
- }
730
- finally {
731
- process.removeListener('SIGINT', handleSigInt);
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
- * Core plan function for programmatic use
386
+ * Plan interactive session for CLI (single turn with logging)
747
387
  */
748
- export async function planCore(options) {
749
- const { prompt, directory, noTest = false } = options;
750
- // Validate directory
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
- success: false,
766
- tickets: [],
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: `Path is not a directory: ${cwd}`,
396
+ error: setup.error,
771
397
  };
772
398
  }
773
- const ticketsPath = generateTicketsPath(cwd);
399
+ const { config, ticketsPath, cwd, prompt } = setup;
774
400
  try {
775
- const result = await interactivePlanSession(prompt, cwd, ticketsPath, undefined, noTest);
776
- return {
777
- success: result.tickets.length > 0,
778
- tickets: result.tickets,
779
- ticketsFile: ticketsPath,
780
- tokensUsed: result.tokensUsed,
781
- cost: result.cost,
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
- success: false,
787
- tickets: [],
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
- const ticketsPath = generateTicketsPath(cwd);
823
- // Run interactive session
824
- const sessionData = await interactivePlanSession(options.prompt, cwd, ticketsPath, logContext, options.noTest ?? false);
825
- // Track metrics
826
- logger.trackTokens(logContext, sessionData.tokensUsed);
827
- // Log successful execution
828
- await logger.complete(logContext, 'success');
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) {