@meistrari/tela-cli 0.1.0

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.
@@ -0,0 +1,701 @@
1
+ import { basename } from 'node:path'
2
+ import type { AgentFeedbackItem, TelaCliConfig } from '../types.ts'
3
+ import { fetchCommitFeedback, fetchRepositoryLookup } from '../agent-api.ts'
4
+ import {
5
+ ensureConfigInteractive,
6
+ type Option,
7
+ type PromptModule as ConfigPromptModule,
8
+ } from '../config.ts'
9
+ import { createUserFacingError } from '../errors.ts'
10
+ import { getDiffStatAgainstHead, getHeadCommitHash, getOriginUrl } from '../git.ts'
11
+ import { CommandError, commandExists, runInteractiveCommand } from '../process.ts'
12
+
13
+ export interface ConfirmOptions {
14
+ message: string
15
+ active?: string
16
+ inactive?: string
17
+ initialValue?: boolean
18
+ }
19
+
20
+ export interface GroupMultiSelectOptions<T> {
21
+ message: string
22
+ options: Record<string, Array<Option<T>>>
23
+ cursorAt?: T
24
+ }
25
+
26
+ export interface PromptModule extends ConfigPromptModule {
27
+ log: {
28
+ success(message: string): void
29
+ }
30
+ note(message: string, title?: string): void
31
+ confirm(options: ConfirmOptions): Promise<boolean | symbol>
32
+ groupMultiselect<T>(options: GroupMultiSelectOptions<T>): Promise<T[] | symbol>
33
+ }
34
+
35
+ export interface FeedbackCommandFlags {
36
+ list?: boolean
37
+ apply?: 'all' | 'select'
38
+ }
39
+
40
+ export interface FeedbackEntry {
41
+ id: string
42
+ number: number
43
+ feedback: AgentFeedbackItem
44
+ actionable: boolean
45
+ }
46
+
47
+ export interface FeedbackData {
48
+ repoRoot: string
49
+ organizationName: string
50
+ repository: string
51
+ commitHash: string
52
+ entries: FeedbackEntry[]
53
+ actionableEntries: FeedbackEntry[]
54
+ }
55
+
56
+ export type FeedbackEmptyState = 'none' | 'no_feedback' | 'informational_only'
57
+ export type FeedbackSelectionMode = 'list' | 'all' | 'select' | 'default'
58
+
59
+ export interface FeedbackSummary {
60
+ organizationName: string
61
+ repository: string
62
+ commitHash: string
63
+ totalFeedbacks: number
64
+ actionableCount: number
65
+ informationalCount: number
66
+ }
67
+
68
+ export interface FeedbackGroupedEntries {
69
+ actionable: FeedbackEntry[]
70
+ informational: FeedbackEntry[]
71
+ }
72
+
73
+ export interface FeedbackReviewModel {
74
+ summary: FeedbackSummary
75
+ groupedEntries: FeedbackGroupedEntries
76
+ emptyState: FeedbackEmptyState
77
+ }
78
+
79
+ export interface FeedbackRunResult {
80
+ ok: true
81
+ organizationName: string
82
+ repository: string
83
+ commitHash: string
84
+ summary: FeedbackSummary
85
+ emptyState: FeedbackEmptyState
86
+ selectionMode: FeedbackSelectionMode
87
+ feedbacks: Array<{
88
+ id: string
89
+ number: number
90
+ status: AgentFeedbackItem['status']
91
+ reason: string
92
+ suggestion: string
93
+ actionable: boolean
94
+ }>
95
+ selectedIds?: string[]
96
+ launchedClaude: boolean
97
+ diffStat?: string
98
+ nextActions: string[]
99
+ }
100
+
101
+ export interface RunFeedbackCommandOptions extends FeedbackCommandFlags {
102
+ cwd?: string
103
+ prompts: PromptModule
104
+ ensureConfigInteractive?: typeof ensureConfigInteractive
105
+ fetchRepositoryLookup?: typeof fetchRepositoryLookup
106
+ fetchCommitFeedback?: typeof fetchCommitFeedback
107
+ getOriginUrl?: typeof getOriginUrl
108
+ getHeadCommitHash?: typeof getHeadCommitHash
109
+ getDiffStatAgainstHead?: typeof getDiffStatAgainstHead
110
+ commandExists?: typeof commandExists
111
+ runInteractiveCommand?: typeof runInteractiveCommand
112
+ }
113
+
114
+ function promptValue<T>(value: T | symbol): T {
115
+ return value as T
116
+ }
117
+
118
+ function truncate(value: string, maxLength: number): string {
119
+ if (value.length <= maxLength) {
120
+ return value
121
+ }
122
+
123
+ return `${value.slice(0, Math.max(0, maxLength - 1)).trimEnd()}...`
124
+ }
125
+
126
+ function firstMeaningfulLine(value: string): string {
127
+ return value
128
+ .split(/\r?\n/)
129
+ .map(line => line.trim())
130
+ .find(Boolean) ?? ''
131
+ }
132
+
133
+ function parseRepositoryNameFromOrigin(originUrl: string): string | null {
134
+ const trimmed = originUrl.trim()
135
+ if (!trimmed) {
136
+ return null
137
+ }
138
+
139
+ if (trimmed.includes('://')) {
140
+ try {
141
+ const url = new URL(trimmed)
142
+ const segments = url.pathname.split('/').filter(Boolean)
143
+ const lastSegment = segments.at(-1)
144
+ return lastSegment ? lastSegment.replace(/\.git$/i, '') : null
145
+ }
146
+ catch {
147
+ return null
148
+ }
149
+ }
150
+
151
+ const scpMatch = trimmed.match(/^[^@]+@[^:]+:(.+)$/)
152
+ if (scpMatch?.[1]) {
153
+ return basename(scpMatch[1]).replace(/\.git$/i, '') || null
154
+ }
155
+
156
+ return basename(trimmed).replace(/\.git$/i, '') || null
157
+ }
158
+
159
+ async function resolveRepositoryName(
160
+ cwd: string,
161
+ getOriginUrlFn: typeof getOriginUrl,
162
+ ): Promise<string> {
163
+ try {
164
+ const originUrl = await getOriginUrlFn(cwd)
165
+ return parseRepositoryNameFromOrigin(originUrl) ?? basename(cwd)
166
+ }
167
+ catch {
168
+ return basename(cwd)
169
+ }
170
+ }
171
+
172
+ function buildFeedbackEntries(feedbacks: AgentFeedbackItem[]): FeedbackEntry[] {
173
+ return feedbacks.map((feedback, index) => ({
174
+ id: String(index + 1),
175
+ number: index + 1,
176
+ feedback,
177
+ actionable: feedback.suggestion.trim().length > 0,
178
+ }))
179
+ }
180
+
181
+ export function getFeedbackEntryTitle(entry: FeedbackEntry): string {
182
+ return firstMeaningfulLine(entry.feedback.suggestion)
183
+ || firstMeaningfulLine(entry.feedback.reason)
184
+ || `Feedback #${entry.number}`
185
+ }
186
+
187
+ export function describeFeedbackSelectionMode(mode: FeedbackSelectionMode): string {
188
+ switch (mode) {
189
+ case 'list':
190
+ return 'Review only'
191
+ case 'all':
192
+ return 'Apply all actionable suggestions'
193
+ case 'select':
194
+ return 'Choose actionable suggestions'
195
+ case 'default':
196
+ return 'Review and edit the actionable selection'
197
+ }
198
+ }
199
+
200
+ export function describeFeedbackEmptyState(emptyState: FeedbackEmptyState): string {
201
+ switch (emptyState) {
202
+ case 'no_feedback':
203
+ return 'No feedback was returned for this commit.'
204
+ case 'informational_only':
205
+ return 'Only informational feedback is available for this commit.'
206
+ case 'none':
207
+ return 'Actionable feedback is available.'
208
+ }
209
+ }
210
+
211
+ export function getFeedbackSelectionMode(options: FeedbackCommandFlags): FeedbackSelectionMode {
212
+ if (options.list) {
213
+ return 'list'
214
+ }
215
+
216
+ if (options.apply === 'all') {
217
+ return 'all'
218
+ }
219
+
220
+ if (options.apply === 'select') {
221
+ return 'select'
222
+ }
223
+
224
+ return 'default'
225
+ }
226
+
227
+ export function buildFeedbackReviewModel(data: FeedbackData): FeedbackReviewModel {
228
+ const groupedEntries: FeedbackGroupedEntries = {
229
+ actionable: data.entries.filter(entry => entry.actionable),
230
+ informational: data.entries.filter(entry => !entry.actionable),
231
+ }
232
+ const summary: FeedbackSummary = {
233
+ organizationName: data.organizationName,
234
+ repository: data.repository,
235
+ commitHash: data.commitHash,
236
+ totalFeedbacks: data.entries.length,
237
+ actionableCount: groupedEntries.actionable.length,
238
+ informationalCount: groupedEntries.informational.length,
239
+ }
240
+
241
+ return {
242
+ summary,
243
+ groupedEntries,
244
+ emptyState: summary.totalFeedbacks === 0
245
+ ? 'no_feedback'
246
+ : summary.actionableCount === 0
247
+ ? 'informational_only'
248
+ : 'none',
249
+ }
250
+ }
251
+
252
+ function formatFeedbackEntryLine(entry: FeedbackEntry): string {
253
+ return `#${entry.number} [${entry.feedback.status}] ${truncate(getFeedbackEntryTitle(entry), 88)}`
254
+ }
255
+
256
+ export function formatFeedbackSummary(summary: FeedbackSummary, emptyState: FeedbackEmptyState): string {
257
+ return [
258
+ `Organization: ${summary.organizationName}`,
259
+ `Repository: ${summary.repository}`,
260
+ `Commit: ${summary.commitHash}`,
261
+ `Feedbacks: ${summary.totalFeedbacks}`,
262
+ `Actionable suggestions: ${summary.actionableCount}`,
263
+ `Informational items: ${summary.informationalCount}`,
264
+ `Review state: ${describeFeedbackEmptyState(emptyState)}`,
265
+ ].join('\n')
266
+ }
267
+
268
+ export function formatFeedbackCatalog(entries: FeedbackEntry[]): string {
269
+ if (entries.length === 0) {
270
+ return 'No feedback returned for this commit.'
271
+ }
272
+
273
+ const groupedEntries = {
274
+ actionable: entries.filter(entry => entry.actionable),
275
+ informational: entries.filter(entry => !entry.actionable),
276
+ }
277
+ const lines: string[] = []
278
+
279
+ if (groupedEntries.actionable.length > 0) {
280
+ lines.push('Actionable')
281
+ for (const entry of groupedEntries.actionable) {
282
+ lines.push(`- ${formatFeedbackEntryLine(entry)}`)
283
+ lines.push(` reason: ${entry.feedback.reason || '<empty>'}`)
284
+ }
285
+ }
286
+
287
+ if (groupedEntries.informational.length > 0) {
288
+ if (lines.length > 0) {
289
+ lines.push('')
290
+ }
291
+
292
+ lines.push('Informational')
293
+ for (const entry of groupedEntries.informational) {
294
+ lines.push(`- ${formatFeedbackEntryLine(entry)}`)
295
+ lines.push(` reason: ${entry.feedback.reason || '<empty>'}`)
296
+ }
297
+ }
298
+
299
+ return lines.join('\n')
300
+ }
301
+
302
+ function buildFeedbackSelectionGroups(entries: FeedbackEntry[]): Record<string, Array<Option<string>>> {
303
+ const groupedEntries = {
304
+ actionable: entries.filter(entry => entry.actionable),
305
+ informational: entries.filter(entry => !entry.actionable),
306
+ }
307
+ const groups: Record<string, Array<Option<string>>> = {}
308
+
309
+ if (groupedEntries.actionable.length > 0) {
310
+ groups.Actionable = groupedEntries.actionable.map(entry => ({
311
+ value: entry.id,
312
+ label: `#${entry.number} ${truncate(getFeedbackEntryTitle(entry), 80)}`,
313
+ hint: truncate(entry.feedback.reason || '<empty>', 60),
314
+ }))
315
+ }
316
+
317
+ if (groupedEntries.informational.length > 0) {
318
+ groups.Informational = groupedEntries.informational.map(entry => ({
319
+ value: entry.id,
320
+ label: `#${entry.number} ${truncate(getFeedbackEntryTitle(entry), 80)}`,
321
+ hint: truncate(entry.feedback.reason || 'No suggestion text', 60),
322
+ }))
323
+ }
324
+
325
+ return groups
326
+ }
327
+
328
+ export function formatFeedbackSelectionSummary(input: {
329
+ selectionMode: FeedbackSelectionMode
330
+ selectedFeedbacks: FeedbackEntry[]
331
+ }): string {
332
+ return [
333
+ `Mode: ${describeFeedbackSelectionMode(input.selectionMode)}`,
334
+ `Selected suggestions: ${input.selectedFeedbacks.length}`,
335
+ `Selected IDs: ${input.selectedFeedbacks.length > 0 ? input.selectedFeedbacks.map(entry => `#${entry.number}`).join(', ') : '<none>'}`,
336
+ ...input.selectedFeedbacks.map(entry => `- ${formatFeedbackEntryLine(entry)}`),
337
+ ].join('\n')
338
+ }
339
+
340
+ export function formatFeedbackOutcomeSummary(input: {
341
+ commitHash: string
342
+ selectionMode: FeedbackSelectionMode
343
+ selectedFeedbacks: FeedbackEntry[]
344
+ }): string {
345
+ return [
346
+ `Commit: ${input.commitHash}`,
347
+ `Mode: ${describeFeedbackSelectionMode(input.selectionMode)}`,
348
+ `Selected suggestions: ${input.selectedFeedbacks.length}`,
349
+ ...input.selectedFeedbacks.map(entry => `- ${formatFeedbackEntryLine(entry)}`),
350
+ ].join('\n')
351
+ }
352
+
353
+ export function buildClaudeFeedbackPrompt(input: {
354
+ repoRoot: string
355
+ organizationName: string
356
+ repository: string
357
+ commitHash: string
358
+ selectedFeedbacks: FeedbackEntry[]
359
+ }): string {
360
+ const feedbackBlocks = input.selectedFeedbacks
361
+ .map(entry => [
362
+ `Feedback #${entry.number}`,
363
+ `Status: ${entry.feedback.status}`,
364
+ `Reason: ${entry.feedback.reason}`,
365
+ `Suggestion: ${entry.feedback.suggestion}`,
366
+ ].join('\n'))
367
+ .join('\n\n')
368
+
369
+ return [
370
+ 'You are working inside a local git repository.',
371
+ '',
372
+ `Repository root: ${input.repoRoot}`,
373
+ `Repository: ${input.organizationName}/${input.repository}`,
374
+ `Feedback commit hash: ${input.commitHash}`,
375
+ '',
376
+ 'Address the selected feedback suggestions below.',
377
+ '- Inspect the repository and make the code changes that best address the selected suggestions.',
378
+ '- Run relevant verification when practical and mention what you checked.',
379
+ '- If a suggestion should not be implemented as written, explain why in your final summary.',
380
+ '- Do not create a git commit or push changes unless explicitly asked.',
381
+ '',
382
+ 'Selected feedback:',
383
+ feedbackBlocks,
384
+ ].join('\n')
385
+ }
386
+
387
+ function serializeFeedbackEntries(entries: FeedbackEntry[]): FeedbackRunResult['feedbacks'] {
388
+ return entries.map(entry => ({
389
+ id: entry.id,
390
+ number: entry.number,
391
+ status: entry.feedback.status,
392
+ reason: entry.feedback.reason,
393
+ suggestion: entry.feedback.suggestion,
394
+ actionable: entry.actionable,
395
+ }))
396
+ }
397
+
398
+ export function buildFeedbackNextActions(input: {
399
+ launchedClaude: boolean
400
+ actionableCount: number
401
+ emptyState: FeedbackEmptyState
402
+ }): string[] {
403
+ if (!input.launchedClaude) {
404
+ if (input.emptyState === 'no_feedback') {
405
+ return ['Push a feedback-producing commit or choose another checkout, then rerun `tela feedback --list`.']
406
+ }
407
+
408
+ if (input.emptyState === 'informational_only') {
409
+ return [
410
+ 'Review the informational feedback above for context.',
411
+ 'Rerun `tela feedback --list` after a later push if you want a fresh snapshot.',
412
+ ]
413
+ }
414
+
415
+ return input.actionableCount > 0
416
+ ? ['Run `tela feedback --apply all` or `tela feedback --apply select` when you are ready to address suggestions.']
417
+ : ['No follow-up action is required for this commit.']
418
+ }
419
+
420
+ return [
421
+ 'Review the local diff before committing the Claude changes.',
422
+ 'Run `tela push` after the feedback-driven changes look correct.',
423
+ 'Rerun `tela feedback --list` after the next push if you want another feedback snapshot.',
424
+ ]
425
+ }
426
+
427
+ export function hasActionableFeedback(entries: FeedbackEntry[]): boolean {
428
+ return entries.some(entry => entry.actionable)
429
+ }
430
+
431
+ export function buildFeedbackRunResult(input: {
432
+ data: FeedbackData
433
+ selectionMode: FeedbackSelectionMode
434
+ launchedClaude: boolean
435
+ selectedFeedbacks?: FeedbackEntry[]
436
+ diffStat?: string
437
+ }): FeedbackRunResult {
438
+ const review = buildFeedbackReviewModel(input.data)
439
+ const selectedFeedbacks = input.selectedFeedbacks ?? []
440
+
441
+ return {
442
+ ok: true,
443
+ organizationName: input.data.organizationName,
444
+ repository: input.data.repository,
445
+ commitHash: input.data.commitHash,
446
+ summary: review.summary,
447
+ emptyState: review.emptyState,
448
+ selectionMode: input.selectionMode,
449
+ feedbacks: serializeFeedbackEntries(input.data.entries),
450
+ selectedIds: selectedFeedbacks.length > 0 ? selectedFeedbacks.map(entry => entry.id) : undefined,
451
+ launchedClaude: input.launchedClaude,
452
+ diffStat: input.diffStat,
453
+ nextActions: buildFeedbackNextActions({
454
+ launchedClaude: input.launchedClaude,
455
+ actionableCount: review.summary.actionableCount,
456
+ emptyState: review.emptyState,
457
+ }),
458
+ }
459
+ }
460
+
461
+ export async function loadFeedbackData(options: {
462
+ cwd?: string
463
+ config: TelaCliConfig
464
+ fetchRepositoryLookup?: typeof fetchRepositoryLookup
465
+ fetchCommitFeedback?: typeof fetchCommitFeedback
466
+ getOriginUrl?: typeof getOriginUrl
467
+ getHeadCommitHash?: typeof getHeadCommitHash
468
+ }): Promise<FeedbackData> {
469
+ const repoRoot = options.cwd ?? process.cwd()
470
+ const fetchRepositoryLookupFn = options.fetchRepositoryLookup ?? fetchRepositoryLookup
471
+ const fetchCommitFeedbackFn = options.fetchCommitFeedback ?? fetchCommitFeedback
472
+ const getOriginUrlFn = options.getOriginUrl ?? getOriginUrl
473
+ const getHeadCommitHashFn = options.getHeadCommitHash ?? getHeadCommitHash
474
+
475
+ const derivedRepository = await resolveRepositoryName(repoRoot, getOriginUrlFn)
476
+ const repoDetails = await fetchRepositoryLookupFn(derivedRepository, options.config)
477
+ const commitHash = await getHeadCommitHashFn(repoRoot)
478
+ const feedbackResponse = await fetchCommitFeedbackFn(
479
+ repoDetails.organization.name,
480
+ repoDetails.agentRepo.repository,
481
+ commitHash,
482
+ options.config,
483
+ )
484
+
485
+ const entries = buildFeedbackEntries(feedbackResponse.feedbacks)
486
+ const actionableEntries = entries.filter(entry => entry.actionable)
487
+
488
+ return {
489
+ repoRoot,
490
+ organizationName: repoDetails.organization.name,
491
+ repository: repoDetails.agentRepo.repository,
492
+ commitHash: feedbackResponse.commitHash,
493
+ entries,
494
+ actionableEntries,
495
+ }
496
+ }
497
+
498
+ export async function launchFeedbackClaude(options: {
499
+ repoRoot: string
500
+ prompt: string
501
+ runInteractiveCommand?: typeof runInteractiveCommand
502
+ }): Promise<void> {
503
+ const runInteractiveCommandFn = options.runInteractiveCommand ?? runInteractiveCommand
504
+
505
+ try {
506
+ await runInteractiveCommandFn(['claude', options.prompt], { cwd: options.repoRoot })
507
+ }
508
+ catch (error) {
509
+ if (error instanceof CommandError) {
510
+ const stdout = error.result.stdout.trim()
511
+ const stderr = error.result.stderr.trim()
512
+ throw createUserFacingError({
513
+ code: 'feedback_claude_failed',
514
+ summary: 'Claude exited before completing the feedback review.',
515
+ details: stderr || stdout || `Claude exited with code ${error.result.exitCode}.`,
516
+ fixes: ['Review the Claude session output above, then rerun `tela feedback` when you are ready.'],
517
+ evidence: [
518
+ `Command: ${error.result.command.join(' ')}`,
519
+ stdout ? `stdout: ${stdout}` : '',
520
+ stderr ? `stderr: ${stderr}` : '',
521
+ ].filter(Boolean),
522
+ cause: error,
523
+ })
524
+ }
525
+
526
+ throw error
527
+ }
528
+ }
529
+
530
+ export async function runFeedbackCommand(options: RunFeedbackCommandOptions): Promise<FeedbackRunResult> {
531
+ const repoRoot = options.cwd ?? process.cwd()
532
+ const prompts = options.prompts
533
+ const ensureConfig = options.ensureConfigInteractive ?? ensureConfigInteractive
534
+ const fetchRepositoryLookupFn = options.fetchRepositoryLookup ?? fetchRepositoryLookup
535
+ const fetchCommitFeedbackFn = options.fetchCommitFeedback ?? fetchCommitFeedback
536
+ const getOriginUrlFn = options.getOriginUrl ?? getOriginUrl
537
+ const getHeadCommitHashFn = options.getHeadCommitHash ?? getHeadCommitHash
538
+ const getDiffStatAgainstHeadFn = options.getDiffStatAgainstHead ?? getDiffStatAgainstHead
539
+ const commandExistsFn = options.commandExists ?? commandExists
540
+ const runInteractiveCommandFn = options.runInteractiveCommand ?? runInteractiveCommand
541
+ const selectionMode = getFeedbackSelectionMode(options)
542
+
543
+ const config: TelaCliConfig = await ensureConfig({ prompts })
544
+ const data = await loadFeedbackData({
545
+ cwd: repoRoot,
546
+ config,
547
+ fetchRepositoryLookup: fetchRepositoryLookupFn,
548
+ fetchCommitFeedback: fetchCommitFeedbackFn,
549
+ getOriginUrl: getOriginUrlFn,
550
+ getHeadCommitHash: getHeadCommitHashFn,
551
+ })
552
+ const review = buildFeedbackReviewModel(data)
553
+ const feedbackEntries = data.entries
554
+ const actionableFeedbacks = data.actionableEntries
555
+
556
+ prompts.note(formatFeedbackSummary(review.summary, review.emptyState), 'Feedback summary')
557
+ prompts.note(formatFeedbackCatalog(feedbackEntries), 'Feedback catalog')
558
+
559
+ if (selectionMode === 'list' || review.emptyState !== 'none') {
560
+ return buildFeedbackRunResult({
561
+ data,
562
+ selectionMode,
563
+ launchedClaude: false,
564
+ })
565
+ }
566
+
567
+ if (!await commandExistsFn('claude')) {
568
+ throw createUserFacingError({
569
+ code: 'feedback_claude_missing',
570
+ summary: 'Claude CLI is not available on PATH.',
571
+ fixes: ['Install the Claude CLI before using `tela feedback --apply ...`.'],
572
+ })
573
+ }
574
+
575
+ let selectedFeedbacks = selectionMode === 'select' ? [] : actionableFeedbacks
576
+ if (selectionMode === 'select') {
577
+ const selectedIds = await prompts.groupMultiselect({
578
+ message: 'Select the suggestions to address with Claude',
579
+ options: buildFeedbackSelectionGroups(feedbackEntries),
580
+ cursorAt: actionableFeedbacks[0]?.id,
581
+ })
582
+
583
+ if (prompts.isCancel(selectedIds)) {
584
+ prompts.cancel('Feedback selection cancelled.')
585
+ return buildFeedbackRunResult({
586
+ data,
587
+ selectionMode,
588
+ launchedClaude: false,
589
+ })
590
+ }
591
+
592
+ const selectedIdSet = new Set(promptValue(selectedIds))
593
+ selectedFeedbacks = actionableFeedbacks.filter(entry => selectedIdSet.has(entry.id))
594
+ }
595
+ else if (selectionMode === 'default') {
596
+ const selectionChoice = await prompts.select<'all' | 'manual' | 'cancel'>({
597
+ message: 'Which suggestions do you want to review with Claude?',
598
+ options: [
599
+ { value: 'all', label: 'Apply all actionable suggestions' },
600
+ { value: 'manual', label: 'Choose suggestions individually' },
601
+ { value: 'cancel', label: 'Finish review without launching Claude' },
602
+ ],
603
+ initialValue: 'all',
604
+ })
605
+
606
+ if (prompts.isCancel(selectionChoice) || promptValue(selectionChoice) === 'cancel') {
607
+ prompts.cancel('Feedback application cancelled.')
608
+ return buildFeedbackRunResult({
609
+ data,
610
+ selectionMode,
611
+ launchedClaude: false,
612
+ })
613
+ }
614
+
615
+ if (promptValue(selectionChoice) === 'manual') {
616
+ const selectedIds = await prompts.groupMultiselect({
617
+ message: 'Select the suggestions to address with Claude',
618
+ options: buildFeedbackSelectionGroups(feedbackEntries),
619
+ cursorAt: actionableFeedbacks[0]?.id,
620
+ })
621
+
622
+ if (prompts.isCancel(selectedIds)) {
623
+ prompts.cancel('Feedback selection cancelled.')
624
+ return buildFeedbackRunResult({
625
+ data,
626
+ selectionMode,
627
+ launchedClaude: false,
628
+ })
629
+ }
630
+
631
+ const selectedIdSet = new Set(promptValue(selectedIds))
632
+ selectedFeedbacks = actionableFeedbacks.filter(entry => selectedIdSet.has(entry.id))
633
+ }
634
+ }
635
+
636
+ prompts.note(formatFeedbackSelectionSummary({
637
+ selectionMode,
638
+ selectedFeedbacks,
639
+ }), 'Selection summary')
640
+
641
+ if (selectedFeedbacks.length === 0) {
642
+ prompts.cancel('Feedback review finished without launching Claude.')
643
+ return buildFeedbackRunResult({
644
+ data,
645
+ selectionMode,
646
+ launchedClaude: false,
647
+ })
648
+ }
649
+
650
+ const prompt = buildClaudeFeedbackPrompt({
651
+ repoRoot: data.repoRoot,
652
+ organizationName: data.organizationName,
653
+ repository: data.repository,
654
+ commitHash: data.commitHash,
655
+ selectedFeedbacks,
656
+ })
657
+
658
+ prompts.note(prompt, 'Claude task')
659
+
660
+ if (selectionMode === 'default') {
661
+ const confirmation = await prompts.confirm({
662
+ message: `Launch Claude to address ${selectedFeedbacks.length} suggestion${selectedFeedbacks.length === 1 ? '' : 's'}?`,
663
+ active: 'apply',
664
+ inactive: 'cancel',
665
+ initialValue: true,
666
+ })
667
+
668
+ if (prompts.isCancel(confirmation) || !promptValue(confirmation)) {
669
+ prompts.cancel('Feedback application cancelled.')
670
+ return buildFeedbackRunResult({
671
+ data,
672
+ selectionMode,
673
+ launchedClaude: false,
674
+ selectedFeedbacks,
675
+ })
676
+ }
677
+ }
678
+
679
+ await launchFeedbackClaude({
680
+ repoRoot: data.repoRoot,
681
+ prompt,
682
+ runInteractiveCommand: runInteractiveCommandFn,
683
+ })
684
+
685
+ const diffStat = (await getDiffStatAgainstHeadFn(repoRoot)) || '<no local diff detected>'
686
+ prompts.note(formatFeedbackOutcomeSummary({
687
+ commitHash: data.commitHash,
688
+ selectionMode,
689
+ selectedFeedbacks,
690
+ }), 'Review outcome')
691
+ prompts.note(diffStat, 'Local diff summary')
692
+ prompts.log.success(`Claude finished reviewing ${selectedFeedbacks.length} suggestion${selectedFeedbacks.length === 1 ? '' : 's'}`)
693
+
694
+ return buildFeedbackRunResult({
695
+ data,
696
+ selectionMode,
697
+ launchedClaude: true,
698
+ selectedFeedbacks,
699
+ diffStat,
700
+ })
701
+ }