@meistrari/tela-cli 0.7.0 → 0.7.4

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 (59) hide show
  1. package/README.md +56 -106
  2. package/dist/commands/auth.d.ts +2 -0
  3. package/dist/commands/context.d.ts +2 -0
  4. package/dist/commands/doctor-auth.d.ts +2 -0
  5. package/dist/commands/env.d.ts +2 -0
  6. package/dist/credentials.d.ts +2 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +7027 -0
  9. package/dist/output.d.ts +13 -0
  10. package/dist/program.d.ts +2 -0
  11. package/package.json +17 -24
  12. package/scripts/install-skills.cjs +127 -0
  13. package/skills/tela-cli/SKILL.md +20 -0
  14. package/skills/tela-cli/references/agent.md +111 -0
  15. package/cli.ts +0 -25
  16. package/postinstall.sh +0 -19
  17. package/src/agent-api.ts +0 -300
  18. package/src/assets.d.ts +0 -4
  19. package/src/auth.ts +0 -432
  20. package/src/claude.ts +0 -101
  21. package/src/commands/doctor.ts +0 -35
  22. package/src/commands/feedback.ts +0 -701
  23. package/src/commands/metadata.ts +0 -242
  24. package/src/commands/push.ts +0 -208
  25. package/src/commands/run.ts +0 -1699
  26. package/src/commands/status.ts +0 -109
  27. package/src/commands/sync.ts +0 -245
  28. package/src/commit-message.ts +0 -69
  29. package/src/config.ts +0 -799
  30. package/src/diagnostics.ts +0 -813
  31. package/src/errors.ts +0 -122
  32. package/src/forgejo.ts +0 -233
  33. package/src/git.ts +0 -457
  34. package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
  35. package/src/ink/bark.tsx +0 -241
  36. package/src/ink/components.tsx +0 -178
  37. package/src/ink/runtime.tsx +0 -90
  38. package/src/ink/screens/feedback-screen.tsx +0 -395
  39. package/src/ink/screens/push-screen.tsx +0 -232
  40. package/src/ink/screens/run-screen.tsx +0 -292
  41. package/src/ink/screens/setup/breadcrumb.tsx +0 -24
  42. package/src/ink/screens/setup/setup-screen.tsx +0 -612
  43. package/src/ink/screens/setup/step-frame.tsx +0 -31
  44. package/src/ink/screens/setup/steps/done-step.tsx +0 -29
  45. package/src/ink/screens/setup/steps/environment-step.tsx +0 -23
  46. package/src/ink/screens/setup/steps/organization-step.tsx +0 -35
  47. package/src/ink/screens/setup/steps/review-step.tsx +0 -78
  48. package/src/ink/screens/setup/steps/running-step.tsx +0 -37
  49. package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -48
  50. package/src/ink/screens/setup/steps/text-field-step.tsx +0 -18
  51. package/src/ink/screens/setup/steps.ts +0 -156
  52. package/src/ink/screens/setup/types.ts +0 -65
  53. package/src/ink/screens/sync-screen.tsx +0 -283
  54. package/src/process.ts +0 -251
  55. package/src/program.ts +0 -2105
  56. package/src/runtime.ts +0 -147
  57. package/src/types.ts +0 -165
  58. package/src/ui.ts +0 -295
  59. package/src/version.ts +0 -3
@@ -1,1699 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { readFile } from 'node:fs/promises'
3
- import { basename, join } from 'node:path'
4
- import {
5
- formatClaudeCliVersion,
6
- inspectClaudeCli,
7
- MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN,
8
- } from '../claude.ts'
9
- import { getRepositoryRoot } from '../git.ts'
10
- import { createUserFacingError } from '../errors.ts'
11
- import {
12
- commandExists,
13
- runCommand,
14
- runInteractiveCommand,
15
- runStreamingCommand,
16
- spawnStreamingCommand,
17
- type CommandResult,
18
- type RunStreamingCommandOptions,
19
- type StreamingCommandHandle,
20
- } from '../process.ts'
21
- import { bulletList, joinBlocks, kvList, resultCard, section, type RenderOptions } from '../ui.ts'
22
-
23
- const INITIAL_PROMPT = 'Follow the instructions and return the result.'
24
- const REQUIRED_AGENT_FILES = [
25
- 'CLAUDE.md',
26
- 'instructions.md',
27
- 'output-format.json',
28
- 'agent.config.json',
29
- ] as const
30
- const TOOL_PROGRESS_MILESTONES = [0, 5, 15, 30] as const
31
-
32
- export type RunMode = 'chat' | 'one-shot'
33
- export type RunOutputSurface = 'plain' | 'viewer' | 'json'
34
- export type RunExecutionStatus = 'starting' | 'running' | 'success' | 'failed' | 'cancelled'
35
-
36
- export interface AgentRunConfig {
37
- model?: string
38
- isMultiturn: boolean
39
- maxRounds?: number
40
- }
41
-
42
- export interface PreparedLocalAgentRun {
43
- repoRoot: string
44
- config: AgentRunConfig
45
- resultKind: 'json' | 'markdown'
46
- resultPath: string
47
- mcpConfigPath?: string
48
- }
49
-
50
- export interface RunStepSummary {
51
- id: string
52
- kind: 'assistant' | 'tool' | 'status' | 'retry' | 'hook' | 'task' | 'artifact' | 'result'
53
- summary: string
54
- detail?: string
55
- notable?: boolean
56
- }
57
-
58
- export interface RunActiveTool {
59
- id: string
60
- name: string
61
- summary: string
62
- detail?: string
63
- elapsedSeconds: number
64
- }
65
-
66
- export interface RunExecutionSummary {
67
- status: RunExecutionStatus
68
- durationMs?: number
69
- durationApiMs?: number
70
- numTurns?: number
71
- stopReason?: string | null
72
- totalCostUsd?: number
73
- permissionDenials: string[]
74
- errors: string[]
75
- notableEvents: string[]
76
- structuredOutputPresent: boolean
77
- }
78
-
79
- export interface RunArtifactSummary {
80
- path: string
81
- kind: 'json' | 'markdown'
82
- value: unknown
83
- }
84
-
85
- export interface RunCommandResult {
86
- ok: true
87
- repoRoot: string
88
- mode: RunMode
89
- config: AgentRunConfig
90
- execution: RunExecutionSummary
91
- steps: RunStepSummary[]
92
- artifact: RunArtifactSummary
93
- resultPath: string
94
- resultKind: 'json' | 'markdown'
95
- resultValue: unknown
96
- }
97
-
98
- export interface RunExecutionSnapshot {
99
- repoRoot: string
100
- mode: RunMode
101
- model?: string
102
- artifactPath: string
103
- startedAtMs: number
104
- status: RunExecutionStatus
105
- currentActivity: string
106
- steps: RunStepSummary[]
107
- activeTools: RunActiveTool[]
108
- execution: RunExecutionSummary
109
- }
110
-
111
- export interface RunExecutionControl {
112
- interrupt(): void
113
- kill(): void
114
- }
115
-
116
- export interface RunLocalAgentCommandOptions {
117
- cwd?: string
118
- contract?: PreparedLocalAgentRun
119
- surface?: RunOutputSurface
120
- commandExists?: typeof commandExists
121
- inspectClaudeCli?: typeof inspectClaudeCli
122
- runCommand?: typeof runCommand
123
- runInteractiveCommand?: typeof runInteractiveCommand
124
- runStreamingCommand?: typeof runStreamingCommand
125
- spawnStreamingCommand?: typeof spawnStreamingCommand
126
- writeStdout?: (message: string) => void
127
- writeStderr?: (message: string) => void
128
- onSnapshot?: (snapshot: RunExecutionSnapshot) => void
129
- onControl?: (control: RunExecutionControl) => void
130
- stdinIsTTY?: boolean
131
- stdoutIsTTY?: boolean
132
- verbose?: boolean
133
- }
134
-
135
- interface AssistantToolUse {
136
- id: string
137
- name: string
138
- input: unknown
139
- summary: string
140
- detail?: string
141
- }
142
-
143
- interface ClaudeResultEvent {
144
- type?: string
145
- subtype?: string
146
- duration_ms?: unknown
147
- duration_api_ms?: unknown
148
- is_error?: boolean
149
- num_turns?: unknown
150
- stop_reason?: unknown
151
- total_cost_usd?: unknown
152
- permission_denials?: unknown
153
- errors?: unknown
154
- structured_output?: unknown
155
- }
156
-
157
- type RunEvent =
158
- | {
159
- kind: 'session_init'
160
- summary: string
161
- detail?: string
162
- model?: string
163
- recordStep: boolean
164
- }
165
- | {
166
- kind: 'assistant_text'
167
- text: string
168
- summary: string
169
- detail?: string
170
- recordStep: true
171
- }
172
- | {
173
- kind: 'tool_batch'
174
- summary: string
175
- detail?: string
176
- tools: RunActiveTool[]
177
- recordStep: true
178
- }
179
- | {
180
- kind: 'tool_progress'
181
- toolUseId: string
182
- toolName: string
183
- summary: string
184
- detail?: string
185
- elapsedSeconds: number
186
- recordStep: false
187
- }
188
- | {
189
- kind: 'status_notice'
190
- summary: string
191
- detail?: string
192
- recordStep: boolean
193
- notable?: boolean
194
- }
195
- | {
196
- kind: 'retry_notice'
197
- summary: string
198
- detail?: string
199
- recordStep: true
200
- notable: true
201
- }
202
- | {
203
- kind: 'hook_notice'
204
- summary: string
205
- detail?: string
206
- recordStep: boolean
207
- }
208
- | {
209
- kind: 'task_notice'
210
- summary: string
211
- detail?: string
212
- recordStep: true
213
- notable?: boolean
214
- }
215
- | {
216
- kind: 'artifact_notice'
217
- summary: string
218
- detail?: string
219
- recordStep: boolean
220
- notable?: boolean
221
- }
222
- | {
223
- kind: 'result'
224
- summary: string
225
- detail?: string
226
- recordStep: true
227
- execution: Partial<RunExecutionSummary> & Pick<RunExecutionSummary, 'status'>
228
- }
229
-
230
- function writeLine(
231
- writer: ((message: string) => void) | undefined,
232
- line: string,
233
- ): void {
234
- const message = `${line}\n`
235
- if (writer) {
236
- writer(message)
237
- return
238
- }
239
-
240
- process.stdout.write(message)
241
- }
242
-
243
- function writeErrorLine(
244
- writer: ((message: string) => void) | undefined,
245
- line: string,
246
- ): void {
247
- const message = `${line}\n`
248
- if (writer) {
249
- writer(message)
250
- return
251
- }
252
-
253
- process.stderr.write(message)
254
- }
255
-
256
- function splitVisibleLines(value: string): string[] {
257
- return value
258
- .split(/\r?\n/)
259
- .map(line => line.trim())
260
- .filter(Boolean)
261
- }
262
-
263
- function truncate(value: string, maxLength = 96): string {
264
- const normalized = value.replace(/\s+/g, ' ').trim()
265
- if (normalized.length <= maxLength) {
266
- return normalized
267
- }
268
-
269
- return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`
270
- }
271
-
272
- function safeString(value: unknown): string | undefined {
273
- return typeof value === 'string' && value.trim().length > 0
274
- ? value.trim()
275
- : undefined
276
- }
277
-
278
- function safeNumber(value: unknown): number | undefined {
279
- return typeof value === 'number' && Number.isFinite(value)
280
- ? value
281
- : undefined
282
- }
283
-
284
- function isRecord(value: unknown): value is Record<string, unknown> {
285
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
286
- }
287
-
288
- function formatDurationMs(durationMs: number | undefined): string {
289
- if (!durationMs || durationMs <= 0) {
290
- return '<unknown>'
291
- }
292
-
293
- if (durationMs < 1000) {
294
- return `${durationMs}ms`
295
- }
296
-
297
- const seconds = durationMs / 1000
298
- if (seconds < 60) {
299
- return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`
300
- }
301
-
302
- const minutes = Math.floor(seconds / 60)
303
- const remainingSeconds = Math.round(seconds % 60)
304
- return `${minutes}m ${remainingSeconds}s`
305
- }
306
-
307
- function formatElapsedSeconds(seconds: number): string {
308
- if (seconds < 60) {
309
- return `${seconds}s`
310
- }
311
-
312
- const minutes = Math.floor(seconds / 60)
313
- const remainder = seconds % 60
314
- return `${minutes}m ${remainder}s`
315
- }
316
-
317
- function summarizePermissionDenial(value: unknown): string {
318
- if (!isRecord(value)) {
319
- return 'Permission denied for a tool call.'
320
- }
321
-
322
- const toolName = safeString(value.tool_name) ?? 'tool'
323
- if (!isRecord(value.tool_input)) {
324
- return `Permission denied: ${toolName}`
325
- }
326
-
327
- const inputSummary = summarizeToolInput(toolName, value.tool_input)
328
- return inputSummary ? `Permission denied: ${toolName} ${inputSummary}` : `Permission denied: ${toolName}`
329
- }
330
-
331
- function parsePermissionDenials(value: unknown): string[] {
332
- if (!Array.isArray(value)) {
333
- return []
334
- }
335
-
336
- return value.map(summarizePermissionDenial)
337
- }
338
-
339
- function summarizeLines(value: string): { summary: string, detail?: string } {
340
- const lines = splitVisibleLines(value)
341
- if (lines.length === 0) {
342
- return { summary: '<empty>' }
343
- }
344
-
345
- const summary = truncate(lines[0] ?? '')
346
- if (lines.length === 1 && summary === lines[0]) {
347
- return { summary }
348
- }
349
-
350
- return {
351
- summary,
352
- detail: lines.join('\n'),
353
- }
354
- }
355
-
356
- function summarizeStructuredOutputPresence(value: unknown): boolean {
357
- return value !== undefined
358
- }
359
-
360
- function summarizeToolInput(toolName: string, input: unknown): string | undefined {
361
- if (!isRecord(input)) {
362
- return undefined
363
- }
364
-
365
- const pathValue = [
366
- input.file_path,
367
- input.path,
368
- input.filename,
369
- input.file,
370
- input.target_file,
371
- input.to_path,
372
- input.from_path,
373
- ].find(candidate => typeof candidate === 'string' && candidate.trim().length > 0)
374
- if (typeof pathValue === 'string') {
375
- return truncate(basename(pathValue))
376
- }
377
-
378
- const commandValue = [
379
- input.command,
380
- input.cmd,
381
- input.script,
382
- input.shell_command,
383
- ].find(candidate => typeof candidate === 'string' && candidate.trim().length > 0)
384
- if (typeof commandValue === 'string') {
385
- return truncate(commandValue.replace(/\s+/g, ' '))
386
- }
387
-
388
- const queryValue = [
389
- input.pattern,
390
- input.query,
391
- input.q,
392
- input.search_query,
393
- ].find(candidate => typeof candidate === 'string' && candidate.trim().length > 0)
394
- if (typeof queryValue === 'string') {
395
- return truncate(queryValue)
396
- }
397
-
398
- const urlValue = [
399
- input.url,
400
- input.href,
401
- ].find(candidate => typeof candidate === 'string' && candidate.trim().length > 0)
402
- if (typeof urlValue === 'string') {
403
- return truncate(urlValue)
404
- }
405
-
406
- const locationValue = safeString(input.location)
407
- if (locationValue) {
408
- return truncate(locationValue)
409
- }
410
-
411
- const titleValue = safeString(input.title)
412
- if (titleValue) {
413
- return truncate(titleValue)
414
- }
415
-
416
- const pageIndex = safeNumber(input.page_index)
417
- if (pageIndex != null) {
418
- return `${toolName} page ${pageIndex}`
419
- }
420
-
421
- return undefined
422
- }
423
-
424
- function summarizeToolUse(name: string, input: unknown): { summary: string, detail?: string } {
425
- const detail = summarizeToolInput(name, input)
426
- return detail
427
- ? { summary: truncate(`${name} ${detail}`), detail: `${name}: ${detail}` }
428
- : { summary: name }
429
- }
430
-
431
- function extractAssistantText(message: unknown): string {
432
- if (!message || typeof message !== 'object') {
433
- return ''
434
- }
435
-
436
- const content = (message as { content?: unknown }).content
437
- if (typeof content === 'string') {
438
- return content
439
- }
440
-
441
- if (!Array.isArray(content)) {
442
- return ''
443
- }
444
-
445
- return content
446
- .flatMap((block) => {
447
- if (!isRecord(block) || block.type !== 'text') {
448
- return []
449
- }
450
-
451
- return typeof block.text === 'string' ? [block.text] : []
452
- })
453
- .join('\n')
454
- }
455
-
456
- function extractAssistantToolUses(message: unknown): AssistantToolUse[] {
457
- if (!message || typeof message !== 'object') {
458
- return []
459
- }
460
-
461
- const content = (message as { content?: unknown }).content
462
- if (!Array.isArray(content)) {
463
- return []
464
- }
465
-
466
- return content.flatMap((block, index) => {
467
- if (!isRecord(block) || block.type !== 'tool_use') {
468
- return []
469
- }
470
-
471
- const name = safeString(block.name)
472
- if (!name) {
473
- return []
474
- }
475
-
476
- const { summary, detail } = summarizeToolUse(name, block.input)
477
- return [{
478
- id: safeString(block.id) ?? `${name}-${index}`,
479
- name,
480
- input: block.input,
481
- summary,
482
- detail,
483
- }]
484
- })
485
- }
486
-
487
- function capitalize(value: string): string {
488
- return value.length > 0
489
- ? `${value[0]?.toUpperCase() ?? ''}${value.slice(1)}`
490
- : value
491
- }
492
-
493
- function pluralize(count: number, singular: string, plural = `${singular}s`): string {
494
- return `${count} ${count === 1 ? singular : plural}`
495
- }
496
-
497
- function categorizeToolName(name: string): 'searches' | 'reads' | 'writes' | 'commands' | 'other' {
498
- const normalized = name.toLowerCase()
499
-
500
- if (/(grep|glob|search|lsp|find)/.test(normalized)) {
501
- return 'searches'
502
- }
503
-
504
- if (/(read|fetch|get|open|view)/.test(normalized)) {
505
- return 'reads'
506
- }
507
-
508
- if (/(write|edit|patch|replace|update|insert|delete|move|format|apply)/.test(normalized)) {
509
- return 'writes'
510
- }
511
-
512
- if (/(bash|shell|exec|command|tmux|terminal)/.test(normalized)) {
513
- return 'commands'
514
- }
515
-
516
- return 'other'
517
- }
518
-
519
- function buildDerivedToolSummary(toolUses: AssistantToolUse[]): string {
520
- if (toolUses.length === 1) {
521
- return toolUses[0]?.summary ?? 'Tool call'
522
- }
523
-
524
- const counts = {
525
- searches: 0,
526
- reads: 0,
527
- writes: 0,
528
- commands: 0,
529
- other: 0,
530
- }
531
-
532
- for (const toolUse of toolUses) {
533
- counts[categorizeToolName(toolUse.name)] += 1
534
- }
535
-
536
- const parts: string[] = []
537
- if (counts.searches > 0) {
538
- parts.push(`searched ${pluralize(counts.searches, 'pattern')}`)
539
- }
540
- if (counts.reads > 0) {
541
- parts.push(`read ${pluralize(counts.reads, 'file')}`)
542
- }
543
- if (counts.writes > 0) {
544
- parts.push(`wrote ${pluralize(counts.writes, 'file')}`)
545
- }
546
- if (counts.commands > 0) {
547
- parts.push(`ran ${pluralize(counts.commands, 'command')}`)
548
- }
549
- if (counts.other > 0) {
550
- parts.push(`${pluralize(counts.other, 'other tool')}`)
551
- }
552
-
553
- return capitalize(parts.join(', '))
554
- }
555
-
556
- function buildToolBatchEvent(
557
- summary: string,
558
- toolUses: AssistantToolUse[],
559
- ): RunEvent {
560
- const detailLines = toolUses
561
- .map(toolUse => toolUse.detail ?? toolUse.summary)
562
- .filter(Boolean)
563
- return {
564
- kind: 'tool_batch',
565
- summary,
566
- detail: detailLines.length > 0 ? detailLines.join('\n') : undefined,
567
- tools: toolUses.map(toolUse => ({
568
- id: toolUse.id,
569
- name: toolUse.name,
570
- summary: toolUse.summary,
571
- detail: toolUse.detail,
572
- elapsedSeconds: 0,
573
- })),
574
- recordStep: true,
575
- }
576
- }
577
-
578
- function normalizeClaudePrintEvent(event: Record<string, unknown>): RunEvent[] {
579
- switch (event.type) {
580
- case 'system': {
581
- switch (event.subtype) {
582
- case 'init':
583
- return [{
584
- kind: 'session_init',
585
- summary: 'Claude session started.',
586
- detail: safeString(event.cwd),
587
- model: safeString(event.model),
588
- recordStep: true,
589
- }]
590
- case 'status': {
591
- const status = safeString(event.status)
592
- if (!status) {
593
- return []
594
- }
595
-
596
- return [{
597
- kind: 'status_notice',
598
- summary: status === 'compacting' ? 'Compacting conversation…' : `Status: ${status}`,
599
- recordStep: status === 'compacting',
600
- notable: status === 'compacting',
601
- }]
602
- }
603
- case 'api_retry': {
604
- const attempt = safeNumber(event.attempt) ?? '?'
605
- const maxRetries = safeNumber(event.max_retries) ?? '?'
606
- const error = safeString(event.error) ?? 'Retrying Claude API request.'
607
- return [{
608
- kind: 'retry_notice',
609
- summary: `Retry ${attempt}/${maxRetries}`,
610
- detail: error,
611
- recordStep: true,
612
- notable: true,
613
- }]
614
- }
615
- case 'hook_started': {
616
- const hookName = safeString(event.hook_name) ?? 'hook'
617
- const hookEvent = safeString(event.hook_event)
618
- return [{
619
- kind: 'hook_notice',
620
- summary: hookEvent ? `Hook started: ${hookName} (${hookEvent})` : `Hook started: ${hookName}`,
621
- recordStep: false,
622
- }]
623
- }
624
- case 'hook_progress': {
625
- const hookName = safeString(event.hook_name) ?? 'hook'
626
- const detail = [safeString(event.output), safeString(event.stdout), safeString(event.stderr)]
627
- .find(Boolean)
628
- return [{
629
- kind: 'hook_notice',
630
- summary: `Hook output: ${hookName}`,
631
- detail,
632
- recordStep: true,
633
- }]
634
- }
635
- case 'task_notification': {
636
- const status = safeString(event.status) ?? 'completed'
637
- const summary = safeString(event.summary)
638
- ?? safeString(event.output_file)
639
- ?? safeString(event.task_id)
640
- ?? 'Task finished'
641
- return [{
642
- kind: 'task_notice',
643
- summary: `Task ${status}: ${summary}`,
644
- detail: safeString(event.output_file),
645
- recordStep: true,
646
- notable: status !== 'completed',
647
- }]
648
- }
649
- case 'files_persisted': {
650
- const failed = Array.isArray(event.failed)
651
- ? event.failed.filter(isRecord)
652
- : []
653
- if (failed.length === 0) {
654
- const persistedCount = Array.isArray(event.files) ? event.files.length : 0
655
- return [{
656
- kind: 'artifact_notice',
657
- summary: `Persisted ${pluralize(persistedCount, 'file')}.`,
658
- recordStep: false,
659
- }]
660
- }
661
-
662
- return [{
663
- kind: 'artifact_notice',
664
- summary: 'Some files could not be persisted.',
665
- detail: failed
666
- .map(entry => `${safeString(entry.filename) ?? '<unknown>'}: ${safeString(entry.error) ?? 'Unknown error'}`)
667
- .join('\n'),
668
- recordStep: true,
669
- notable: true,
670
- }]
671
- }
672
- default:
673
- return []
674
- }
675
- }
676
- case 'assistant': {
677
- const normalized: RunEvent[] = []
678
- const text = extractAssistantText(event.message)
679
- if (text.trim()) {
680
- const summary = summarizeLines(text)
681
- normalized.push({
682
- kind: 'assistant_text',
683
- text,
684
- summary: summary.summary,
685
- detail: summary.detail,
686
- recordStep: true,
687
- })
688
- }
689
-
690
- const toolUses = extractAssistantToolUses(event.message)
691
- if (toolUses.length > 0) {
692
- normalized.push(buildToolBatchEvent(buildDerivedToolSummary(toolUses), toolUses))
693
- }
694
-
695
- return normalized
696
- }
697
- case 'streamlined_text': {
698
- const text = safeString(event.text)
699
- if (!text) {
700
- return []
701
- }
702
-
703
- const summary = summarizeLines(text)
704
- return [{
705
- kind: 'assistant_text',
706
- text,
707
- summary: summary.summary,
708
- detail: summary.detail,
709
- recordStep: true,
710
- }]
711
- }
712
- case 'tool_use_summary': {
713
- const summary = safeString(event.summary)
714
- return summary
715
- ? [buildToolBatchEvent(summary, [])]
716
- : []
717
- }
718
- case 'streamlined_tool_use_summary': {
719
- const summary = safeString(event.tool_summary)
720
- return summary
721
- ? [buildToolBatchEvent(summary, [])]
722
- : []
723
- }
724
- case 'tool_progress': {
725
- const toolUseId = safeString(event.tool_use_id)
726
- const toolName = safeString(event.tool_name)
727
- const elapsedSeconds = safeNumber(event.elapsed_time_seconds)
728
- if (!toolUseId || !toolName || elapsedSeconds == null) {
729
- return []
730
- }
731
-
732
- return [{
733
- kind: 'tool_progress',
734
- toolUseId,
735
- toolName,
736
- summary: `${toolName} running for ${formatElapsedSeconds(elapsedSeconds)}`,
737
- detail: safeString(event.task_id),
738
- elapsedSeconds,
739
- recordStep: false,
740
- }]
741
- }
742
- case 'result': {
743
- const subtype = safeString(event.subtype) ?? 'success'
744
- const status: RunExecutionStatus = subtype === 'success'
745
- ? 'success'
746
- : 'failed'
747
- const durationMs = safeNumber(event.duration_ms)
748
- const detail = Array.isArray(event.errors)
749
- ? event.errors.filter((value): value is string => typeof value === 'string' && value.trim().length > 0).join('\n')
750
- : undefined
751
-
752
- return [{
753
- kind: 'result',
754
- summary: status === 'success'
755
- ? `Run completed in ${formatDurationMs(durationMs)}.`
756
- : `Run failed after ${formatDurationMs(durationMs)}.`,
757
- detail,
758
- recordStep: true,
759
- execution: {
760
- status,
761
- durationMs,
762
- durationApiMs: safeNumber(event.duration_api_ms),
763
- numTurns: safeNumber(event.num_turns),
764
- stopReason: safeString(event.stop_reason) ?? null,
765
- totalCostUsd: safeNumber(event.total_cost_usd),
766
- permissionDenials: parsePermissionDenials(event.permission_denials),
767
- errors: Array.isArray(event.errors)
768
- ? event.errors.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
769
- : [],
770
- structuredOutputPresent: summarizeStructuredOutputPresence(event.structured_output),
771
- notableEvents: [],
772
- },
773
- }]
774
- }
775
- default:
776
- return []
777
- }
778
- }
779
-
780
- class RunStateTracker {
781
- private readonly steps: RunStepSummary[] = []
782
- private readonly activeTools = new Map<string, RunActiveTool>()
783
- private readonly execution: RunExecutionSummary
784
- private stepIndex = 0
785
- private status: RunExecutionStatus = 'starting'
786
- private currentActivity = 'Preparing Claude run…'
787
- private model?: string
788
-
789
- constructor(
790
- private readonly prepared: PreparedLocalAgentRun,
791
- private readonly mode: RunMode,
792
- private readonly startedAtMs = Date.now(),
793
- ) {
794
- this.execution = {
795
- status: 'starting',
796
- permissionDenials: [],
797
- errors: [],
798
- notableEvents: [],
799
- structuredOutputPresent: false,
800
- }
801
- this.model = prepared.config.model
802
- }
803
-
804
- private addStep(
805
- kind: RunStepSummary['kind'],
806
- summary: string,
807
- detail?: string,
808
- notable = false,
809
- ): void {
810
- this.steps.push({
811
- id: `step-${++this.stepIndex}`,
812
- kind,
813
- summary,
814
- detail,
815
- notable,
816
- })
817
- }
818
-
819
- private note(message: string | undefined): void {
820
- if (!message) {
821
- return
822
- }
823
-
824
- if (!this.execution.notableEvents.includes(message)) {
825
- this.execution.notableEvents.push(message)
826
- }
827
- }
828
-
829
- private clearActiveTools(): void {
830
- this.activeTools.clear()
831
- }
832
-
833
- update(event: RunEvent): RunExecutionSnapshot {
834
- switch (event.kind) {
835
- case 'session_init':
836
- this.status = 'running'
837
- this.execution.status = 'running'
838
- this.currentActivity = event.summary
839
- this.model = event.model ?? this.model
840
- if (event.recordStep) {
841
- this.addStep('status', event.summary, event.detail)
842
- }
843
- break
844
- case 'assistant_text':
845
- this.clearActiveTools()
846
- this.status = 'running'
847
- this.execution.status = 'running'
848
- this.currentActivity = event.summary
849
- this.addStep('assistant', event.summary, event.detail)
850
- break
851
- case 'tool_batch':
852
- this.clearActiveTools()
853
- this.status = 'running'
854
- this.execution.status = 'running'
855
- this.currentActivity = event.summary
856
- this.addStep('tool', event.summary, event.detail)
857
- for (const tool of event.tools) {
858
- this.activeTools.set(tool.id, tool)
859
- }
860
- break
861
- case 'tool_progress': {
862
- const previous = this.activeTools.get(event.toolUseId)
863
- this.activeTools.set(event.toolUseId, {
864
- id: event.toolUseId,
865
- name: event.toolName,
866
- summary: previous?.summary ?? event.toolName,
867
- detail: previous?.detail ?? event.detail,
868
- elapsedSeconds: event.elapsedSeconds,
869
- })
870
- this.status = 'running'
871
- this.execution.status = 'running'
872
- this.currentActivity = event.summary
873
- break
874
- }
875
- case 'status_notice':
876
- this.status = 'running'
877
- this.execution.status = 'running'
878
- this.currentActivity = event.summary
879
- if (event.recordStep) {
880
- this.addStep('status', event.summary, event.detail, Boolean(event.notable))
881
- }
882
- if (event.notable) {
883
- this.note(event.summary)
884
- }
885
- break
886
- case 'retry_notice':
887
- this.status = 'running'
888
- this.execution.status = 'running'
889
- this.currentActivity = event.summary
890
- this.addStep('retry', event.summary, event.detail, true)
891
- this.note(event.summary)
892
- break
893
- case 'hook_notice':
894
- this.status = 'running'
895
- this.execution.status = 'running'
896
- this.currentActivity = event.summary
897
- if (event.recordStep) {
898
- this.addStep('hook', event.summary, event.detail)
899
- }
900
- break
901
- case 'task_notice':
902
- this.status = 'running'
903
- this.execution.status = 'running'
904
- this.currentActivity = event.summary
905
- this.addStep('task', event.summary, event.detail, Boolean(event.notable))
906
- if (event.notable) {
907
- this.note(event.summary)
908
- }
909
- break
910
- case 'artifact_notice':
911
- this.status = 'running'
912
- this.execution.status = 'running'
913
- this.currentActivity = event.summary
914
- if (event.recordStep) {
915
- this.addStep('artifact', event.summary, event.detail, Boolean(event.notable))
916
- }
917
- if (event.notable) {
918
- this.note(event.summary)
919
- }
920
- break
921
- case 'result':
922
- this.clearActiveTools()
923
- this.status = event.execution.status
924
- this.currentActivity = event.summary
925
- this.execution.status = event.execution.status
926
- this.execution.durationMs = event.execution.durationMs
927
- this.execution.durationApiMs = event.execution.durationApiMs
928
- this.execution.numTurns = event.execution.numTurns
929
- this.execution.stopReason = event.execution.stopReason
930
- this.execution.totalCostUsd = event.execution.totalCostUsd
931
- this.execution.permissionDenials = [...(event.execution.permissionDenials ?? [])]
932
- this.execution.errors = [...(event.execution.errors ?? [])]
933
- this.execution.structuredOutputPresent = Boolean(event.execution.structuredOutputPresent)
934
- for (const message of event.execution.notableEvents ?? []) {
935
- this.note(message)
936
- }
937
- this.addStep('result', event.summary, event.detail, event.execution.status !== 'success')
938
- break
939
- }
940
-
941
- return this.snapshot()
942
- }
943
-
944
- finalize(status: Extract<RunExecutionStatus, 'success' | 'failed' | 'cancelled'>, detail?: {
945
- errors?: string[]
946
- durationMs?: number
947
- durationApiMs?: number
948
- numTurns?: number
949
- stopReason?: string | null
950
- totalCostUsd?: number
951
- permissionDenials?: string[]
952
- structuredOutputPresent?: boolean
953
- summary?: string
954
- }): RunExecutionSnapshot {
955
- this.clearActiveTools()
956
- this.status = status
957
- this.execution.status = status
958
- this.execution.errors = detail?.errors ?? this.execution.errors
959
- this.execution.durationMs = detail?.durationMs ?? this.execution.durationMs
960
- this.execution.durationApiMs = detail?.durationApiMs ?? this.execution.durationApiMs
961
- this.execution.numTurns = detail?.numTurns ?? this.execution.numTurns
962
- this.execution.stopReason = detail?.stopReason ?? this.execution.stopReason
963
- this.execution.totalCostUsd = detail?.totalCostUsd ?? this.execution.totalCostUsd
964
- this.execution.permissionDenials = detail?.permissionDenials ?? this.execution.permissionDenials
965
- this.execution.structuredOutputPresent = detail?.structuredOutputPresent ?? this.execution.structuredOutputPresent
966
-
967
- const summary = detail?.summary ?? (
968
- status === 'success'
969
- ? `Run completed in ${formatDurationMs(this.execution.durationMs)}.`
970
- : status === 'cancelled'
971
- ? 'Run cancelled.'
972
- : `Run failed after ${formatDurationMs(this.execution.durationMs)}.`
973
- )
974
- this.currentActivity = summary
975
-
976
- const lastStep = this.steps.at(-1)
977
- if (!lastStep || lastStep.kind !== 'result' || lastStep.summary !== summary) {
978
- this.addStep('result', summary, detail?.errors?.join('\n'), status !== 'success')
979
- }
980
-
981
- return this.snapshot()
982
- }
983
-
984
- snapshot(): RunExecutionSnapshot {
985
- return {
986
- repoRoot: this.prepared.repoRoot,
987
- mode: this.mode,
988
- model: this.model,
989
- artifactPath: this.prepared.resultPath,
990
- startedAtMs: this.startedAtMs,
991
- status: this.status,
992
- currentActivity: this.currentActivity,
993
- steps: this.steps.map(step => ({ ...step })),
994
- activeTools: Array.from(this.activeTools.values()).map(tool => ({ ...tool })),
995
- execution: {
996
- status: this.execution.status,
997
- durationMs: this.execution.durationMs,
998
- durationApiMs: this.execution.durationApiMs,
999
- numTurns: this.execution.numTurns,
1000
- stopReason: this.execution.stopReason,
1001
- totalCostUsd: this.execution.totalCostUsd,
1002
- permissionDenials: [...this.execution.permissionDenials],
1003
- errors: [...this.execution.errors],
1004
- notableEvents: [...this.execution.notableEvents],
1005
- structuredOutputPresent: this.execution.structuredOutputPresent,
1006
- },
1007
- }
1008
- }
1009
- }
1010
-
1011
- class PlainTranscriptRenderer {
1012
- private readonly nextMilestoneIndex = new Map<string, number>()
1013
-
1014
- render(
1015
- event: RunEvent,
1016
- snapshot: RunExecutionSnapshot,
1017
- verbose = false,
1018
- ): string[] {
1019
- switch (event.kind) {
1020
- case 'session_init':
1021
- return [snapshot.model ? `Claude session started (${snapshot.model}).` : event.summary]
1022
- case 'assistant_text':
1023
- return splitVisibleLines(event.text).map(line => `Assistant: ${line}`)
1024
- case 'tool_batch': {
1025
- const lines = [`Tools: ${event.summary}`]
1026
- if (verbose && event.detail) {
1027
- lines.push(...splitVisibleLines(event.detail).map(line => ` ${line}`))
1028
- }
1029
- return lines
1030
- }
1031
- case 'tool_progress': {
1032
- const nextIndex = this.nextMilestoneIndex.get(event.toolUseId) ?? 0
1033
- const milestone = TOOL_PROGRESS_MILESTONES[nextIndex]
1034
- const shouldEmit = milestone != null
1035
- ? event.elapsedSeconds >= milestone
1036
- : event.elapsedSeconds % 60 === 0
1037
- if (!shouldEmit) {
1038
- return []
1039
- }
1040
-
1041
- if (milestone != null) {
1042
- this.nextMilestoneIndex.set(event.toolUseId, nextIndex + 1)
1043
- }
1044
-
1045
- return [`Working: ${event.toolName} (${formatElapsedSeconds(event.elapsedSeconds)})`]
1046
- }
1047
- case 'status_notice':
1048
- return event.recordStep ? [event.summary] : []
1049
- case 'retry_notice': {
1050
- const lines = [event.summary]
1051
- if (event.detail) {
1052
- lines.push(` ${event.detail}`)
1053
- }
1054
- return lines
1055
- }
1056
- case 'hook_notice':
1057
- if (!event.recordStep) {
1058
- return []
1059
- }
1060
- return [event.summary, ...(verbose && event.detail ? splitVisibleLines(event.detail).map(line => ` ${line}`) : [])]
1061
- case 'task_notice':
1062
- return [event.summary, ...(verbose && event.detail ? [` ${event.detail}`] : [])]
1063
- case 'artifact_notice':
1064
- if (!event.recordStep && !verbose) {
1065
- return []
1066
- }
1067
- return [event.summary, ...(verbose && event.detail ? splitVisibleLines(event.detail).map(line => ` ${line}`) : [])]
1068
- case 'result':
1069
- return [event.summary]
1070
- default:
1071
- return []
1072
- }
1073
- }
1074
- }
1075
-
1076
- function parseJsonObjectFile(
1077
- path: string,
1078
- content: string,
1079
- invalidSummary: string,
1080
- ): Record<string, unknown> {
1081
- let parsed: unknown
1082
- try {
1083
- parsed = JSON.parse(content)
1084
- }
1085
- catch (error) {
1086
- throw createUserFacingError({
1087
- code: 'run_invalid_json',
1088
- summary: invalidSummary,
1089
- details: error instanceof Error ? error.message : String(error),
1090
- fixes: [`Fix the JSON syntax in \`${path}\`.`],
1091
- })
1092
- }
1093
-
1094
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1095
- throw createUserFacingError({
1096
- code: 'run_invalid_json_shape',
1097
- summary: invalidSummary,
1098
- fixes: [`Update \`${path}\` so it contains a JSON object.`],
1099
- })
1100
- }
1101
-
1102
- return parsed as Record<string, unknown>
1103
- }
1104
-
1105
- function parseAgentConfig(path: string, content: string): AgentRunConfig {
1106
- const parsed = parseJsonObjectFile(path, content, 'agent.config.json must be a valid JSON object.')
1107
-
1108
- if (parsed.model !== undefined && typeof parsed.model !== 'string') {
1109
- throw createUserFacingError({
1110
- code: 'run_invalid_model',
1111
- summary: 'agent.config.json has an invalid `model` value.',
1112
- fixes: ['Set `model` to a string, or remove it from agent.config.json.'],
1113
- })
1114
- }
1115
-
1116
- if (parsed.isMultiturn !== undefined && typeof parsed.isMultiturn !== 'boolean') {
1117
- throw createUserFacingError({
1118
- code: 'run_invalid_multiturn',
1119
- summary: 'agent.config.json has an invalid `isMultiturn` value.',
1120
- fixes: ['Set `isMultiturn` to `true` or `false`, or remove it from agent.config.json.'],
1121
- })
1122
- }
1123
-
1124
- if (parsed.maxRounds !== undefined && (!Number.isInteger(parsed.maxRounds) || Number(parsed.maxRounds) <= 0)) {
1125
- throw createUserFacingError({
1126
- code: 'run_invalid_max_rounds',
1127
- summary: 'agent.config.json has an invalid `maxRounds` value.',
1128
- fixes: ['Set `maxRounds` to a positive integer, or remove it from agent.config.json.'],
1129
- })
1130
- }
1131
-
1132
- const model = typeof parsed.model === 'string' && parsed.model.trim().length > 0
1133
- ? parsed.model.trim()
1134
- : undefined
1135
-
1136
- return {
1137
- model,
1138
- isMultiturn: parsed.isMultiturn === true,
1139
- maxRounds: typeof parsed.maxRounds === 'number' ? parsed.maxRounds : undefined,
1140
- }
1141
- }
1142
-
1143
- const CLAUDE_RUN_ENV_DENYLIST = new Set([
1144
- 'CLAUDE_CONFIG_DIR',
1145
- 'CLAUDE_ENV_FILE',
1146
- 'ANTHROPIC_MODEL',
1147
- 'ANTHROPIC_CUSTOM_MODEL_OPTION',
1148
- 'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
1149
- 'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
1150
- 'ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES',
1151
- 'ANTHROPIC_DEFAULT_OPUS_MODEL',
1152
- 'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
1153
- 'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
1154
- 'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
1155
- 'ANTHROPIC_DEFAULT_SONNET_MODEL',
1156
- 'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
1157
- 'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
1158
- 'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
1159
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
1160
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
1161
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
1162
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
1163
- 'CLAUDE_CODE_SUBAGENT_MODEL',
1164
- ])
1165
-
1166
- function buildClaudeRunEnvironment(
1167
- baseEnv: NodeJS.ProcessEnv = process.env,
1168
- ): Record<string, string | undefined> {
1169
- return Object.fromEntries(
1170
- Object.entries(baseEnv).filter(([key]) => !CLAUDE_RUN_ENV_DENYLIST.has(key)),
1171
- )
1172
- }
1173
-
1174
- function buildUnsupportedClaudeVersionError(detail: string | undefined): ReturnType<typeof createUserFacingError> {
1175
- const requiredVersion = formatClaudeCliVersion(MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN)
1176
-
1177
- return createUserFacingError({
1178
- code: 'run_claude_version_unsupported',
1179
- summary: `\`tela run\` requires Claude CLI ${requiredVersion} or newer for project-scoped runs.`,
1180
- details: detail,
1181
- fixes: [
1182
- 'Update Claude CLI before using `tela run`.',
1183
- 'Run `claude --version` to confirm the installed version.',
1184
- ],
1185
- })
1186
- }
1187
-
1188
- function buildClaudeBaseCommand(prepared: PreparedLocalAgentRun): string[] {
1189
- const command = [
1190
- 'claude',
1191
- '--setting-sources',
1192
- 'project',
1193
- ]
1194
-
1195
- if (prepared.mcpConfigPath) {
1196
- command.push('--strict-mcp-config', '--mcp-config', prepared.mcpConfigPath)
1197
- }
1198
-
1199
- if (prepared.config.model) {
1200
- command.push('--model', prepared.config.model)
1201
- }
1202
-
1203
- return command
1204
- }
1205
-
1206
- function buildClaudeCommand(
1207
- prepared: PreparedLocalAgentRun,
1208
- mode: RunMode,
1209
- ): string[] {
1210
- const command = buildClaudeBaseCommand(prepared)
1211
-
1212
- if (mode === 'one-shot') {
1213
- command.push(
1214
- '-p',
1215
- '--output-format',
1216
- 'stream-json',
1217
- '--verbose',
1218
- )
1219
- }
1220
-
1221
- command.push(INITIAL_PROMPT)
1222
- return command
1223
- }
1224
-
1225
- async function resolveRepositoryRoot(cwd: string): Promise<string> {
1226
- try {
1227
- return await getRepositoryRoot(cwd)
1228
- }
1229
- catch (error) {
1230
- if (error instanceof Error && error.message.startsWith('Not a git repository:')) {
1231
- throw createUserFacingError({
1232
- code: 'run_outside_repo',
1233
- summary: 'Run must be executed from inside a git repository.',
1234
- fixes: [
1235
- 'Change into an agent repository, or run `tela sync <repo>` first.',
1236
- ],
1237
- })
1238
- }
1239
-
1240
- throw error
1241
- }
1242
- }
1243
-
1244
- async function validateRequiredFiles(repoRoot: string): Promise<void> {
1245
- const missing: string[] = []
1246
-
1247
- for (const file of REQUIRED_AGENT_FILES) {
1248
- try {
1249
- await readFile(join(repoRoot, file), 'utf-8')
1250
- }
1251
- catch {
1252
- missing.push(file)
1253
- }
1254
- }
1255
-
1256
- if (missing.length === 0) {
1257
- return
1258
- }
1259
-
1260
- throw createUserFacingError({
1261
- code: 'run_missing_contract_files',
1262
- summary: 'Agent repository is missing required files.',
1263
- fixes: missing.map(file => `Add \`${file}\` to the repository root.`),
1264
- evidence: missing.map(file => `Missing: ${join(repoRoot, file)}`),
1265
- })
1266
- }
1267
-
1268
- export async function prepareLocalAgentRun(options: {
1269
- cwd?: string
1270
- commandExists?: typeof commandExists
1271
- inspectClaudeCli?: typeof inspectClaudeCli
1272
- runCommand?: typeof runCommand
1273
- } = {}): Promise<PreparedLocalAgentRun> {
1274
- const cwd = options.cwd ?? process.cwd()
1275
- const repoRoot = await resolveRepositoryRoot(cwd)
1276
- const inspectClaudeCliFn = options.inspectClaudeCli ?? inspectClaudeCli
1277
-
1278
- const claudeInspection = await inspectClaudeCliFn({
1279
- commandExistsFn: options.commandExists,
1280
- runCommandFn: options.runCommand,
1281
- })
1282
-
1283
- if (!claudeInspection.available) {
1284
- throw createUserFacingError({
1285
- code: 'run_claude_missing',
1286
- summary: 'Claude CLI is not available on PATH.',
1287
- fixes: ['Install the Claude CLI before using `tela run`.'],
1288
- })
1289
- }
1290
-
1291
- if (!claudeInspection.supportsProjectScopedRun) {
1292
- throw buildUnsupportedClaudeVersionError(claudeInspection.detail)
1293
- }
1294
-
1295
- await validateRequiredFiles(repoRoot)
1296
-
1297
- const [outputFormatContent, agentConfigContent] = await Promise.all([
1298
- readFile(join(repoRoot, 'output-format.json'), 'utf-8'),
1299
- readFile(join(repoRoot, 'agent.config.json'), 'utf-8'),
1300
- ])
1301
-
1302
- const outputFormat = parseJsonObjectFile(
1303
- 'output-format.json',
1304
- outputFormatContent,
1305
- 'output-format.json must be a valid JSON object.',
1306
- )
1307
- const config = parseAgentConfig('agent.config.json', agentConfigContent)
1308
- const resultKind = Object.keys(outputFormat).length > 0 ? 'json' : 'markdown'
1309
-
1310
- return {
1311
- repoRoot,
1312
- config,
1313
- resultKind,
1314
- resultPath: join(repoRoot, 'output', resultKind === 'json' ? 'result.json' : 'result.md'),
1315
- mcpConfigPath: existsSync(join(repoRoot, '.mcp.json'))
1316
- ? join(repoRoot, '.mcp.json')
1317
- : undefined,
1318
- }
1319
- }
1320
-
1321
- export function createInitialRunSnapshot(
1322
- prepared: PreparedLocalAgentRun,
1323
- mode: RunMode = prepared.config.isMultiturn ? 'chat' : 'one-shot',
1324
- ): RunExecutionSnapshot {
1325
- return {
1326
- repoRoot: prepared.repoRoot,
1327
- mode,
1328
- model: prepared.config.model,
1329
- artifactPath: prepared.resultPath,
1330
- startedAtMs: Date.now(),
1331
- status: 'starting',
1332
- currentActivity: mode === 'chat'
1333
- ? 'Preparing Claude chat handoff…'
1334
- : 'Validating repository contract…',
1335
- steps: [],
1336
- activeTools: [],
1337
- execution: {
1338
- status: 'starting',
1339
- permissionDenials: [],
1340
- errors: [],
1341
- notableEvents: [],
1342
- structuredOutputPresent: false,
1343
- },
1344
- }
1345
- }
1346
-
1347
- function snapshotStepEvidence(snapshot: RunExecutionSnapshot): string[] {
1348
- return snapshot.steps
1349
- .slice(-5)
1350
- .map(step => step.summary)
1351
- }
1352
-
1353
- function buildRunFailureError(
1354
- result: CommandResult,
1355
- snapshot: RunExecutionSnapshot,
1356
- ): ReturnType<typeof createUserFacingError> {
1357
- const details = snapshot.execution.errors[0]
1358
- ?? result.stderr.trim()
1359
- ?? result.stdout.trim()
1360
- ?? `Claude exited with code ${result.exitCode}.`
1361
-
1362
- return createUserFacingError({
1363
- code: 'run_failed',
1364
- summary: 'Claude run failed.',
1365
- details,
1366
- fixes: [
1367
- 'Review the streamed Claude output above for the failing step.',
1368
- 'Fix the repository instructions or inputs, then rerun `tela run`.',
1369
- ],
1370
- evidence: [
1371
- `Command: ${result.command.join(' ')}`,
1372
- ...snapshotStepEvidence(snapshot),
1373
- ...snapshot.execution.errors,
1374
- result.stderr.trim() ? `stderr: ${result.stderr.trim()}` : '',
1375
- ].filter(Boolean),
1376
- })
1377
- }
1378
-
1379
- function buildRunCancelledError(snapshot: RunExecutionSnapshot): ReturnType<typeof createUserFacingError> {
1380
- return createUserFacingError({
1381
- code: 'run_cancelled',
1382
- summary: 'Claude run was cancelled.',
1383
- details: snapshot.currentActivity,
1384
- fixes: ['Run `tela run` again when you are ready to continue.'],
1385
- evidence: snapshotStepEvidence(snapshot),
1386
- exitCode: 130,
1387
- })
1388
- }
1389
-
1390
- async function readResultArtifact(
1391
- prepared: PreparedLocalAgentRun,
1392
- snapshot: RunExecutionSnapshot,
1393
- ): Promise<RunCommandResult> {
1394
- let content: string
1395
- try {
1396
- content = await readFile(prepared.resultPath, 'utf-8')
1397
- }
1398
- catch {
1399
- throw createUserFacingError({
1400
- code: 'run_missing_result',
1401
- summary: `Expected result artifact was not created: ${prepared.resultPath}`,
1402
- details: joinBlocks([
1403
- `Last activity: ${snapshot.currentActivity}`,
1404
- snapshot.execution.numTurns != null ? `Turns: ${snapshot.execution.numTurns}` : undefined,
1405
- ]),
1406
- fixes: [
1407
- 'Ensure the agent writes the required output file before exiting.',
1408
- 'Rerun `tela run` after fixing the repository instructions.',
1409
- ],
1410
- evidence: snapshotStepEvidence(snapshot),
1411
- })
1412
- }
1413
-
1414
- let resultValue: unknown = content
1415
- if (prepared.resultKind === 'json') {
1416
- try {
1417
- resultValue = JSON.parse(content)
1418
- }
1419
- catch (error) {
1420
- throw createUserFacingError({
1421
- code: 'run_invalid_result_json',
1422
- summary: 'output/result.json is not valid JSON.',
1423
- details: error instanceof Error ? error.message : String(error),
1424
- fixes: ['Ensure the agent writes valid JSON to output/result.json.'],
1425
- evidence: snapshotStepEvidence(snapshot),
1426
- })
1427
- }
1428
- }
1429
-
1430
- const executionStatus = snapshot.execution.status === 'cancelled'
1431
- ? 'cancelled'
1432
- : snapshot.execution.status === 'failed'
1433
- ? 'failed'
1434
- : 'success'
1435
-
1436
- return {
1437
- ok: true,
1438
- repoRoot: prepared.repoRoot,
1439
- mode: prepared.config.isMultiturn ? 'chat' : 'one-shot',
1440
- config: prepared.config,
1441
- execution: {
1442
- ...snapshot.execution,
1443
- status: executionStatus,
1444
- },
1445
- steps: snapshot.steps,
1446
- artifact: {
1447
- path: prepared.resultPath,
1448
- kind: prepared.resultKind,
1449
- value: resultValue,
1450
- },
1451
- resultPath: prepared.resultPath,
1452
- resultKind: prepared.resultKind,
1453
- resultValue,
1454
- }
1455
- }
1456
-
1457
- async function runInteractiveAgent(
1458
- prepared: PreparedLocalAgentRun,
1459
- runInteractiveCommandFn: typeof runInteractiveCommand,
1460
- options: RunLocalAgentCommandOptions,
1461
- ): Promise<void> {
1462
- if (options.surface === 'json') {
1463
- throw createUserFacingError({
1464
- code: 'run_multiturn_json_unsupported',
1465
- summary: 'Multiturn run requires an interactive TTY in the current hybrid design.',
1466
- fixes: ['Run `tela run` from an interactive terminal without `--json`.'],
1467
- })
1468
- }
1469
-
1470
- const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY)
1471
- const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY)
1472
- if (!stdinIsTTY || !stdoutIsTTY) {
1473
- throw createUserFacingError({
1474
- code: 'run_multiturn_tty_required',
1475
- summary: 'Multiturn run requires an interactive TTY in the current hybrid design.',
1476
- fixes: ['Run `tela run` from an interactive terminal.'],
1477
- })
1478
- }
1479
-
1480
- if (options.surface === 'plain') {
1481
- writeLine(options.writeStdout, 'Launching Claude in interactive chat mode. Tela will resume after Claude exits.')
1482
- }
1483
-
1484
- await runInteractiveCommandFn(buildClaudeCommand(prepared, 'chat'), {
1485
- cwd: prepared.repoRoot,
1486
- env: buildClaudeRunEnvironment(),
1487
- })
1488
- }
1489
-
1490
- async function runOneShotAgent(
1491
- prepared: PreparedLocalAgentRun,
1492
- options: RunLocalAgentCommandOptions,
1493
- tracker: RunStateTracker,
1494
- ): Promise<RunExecutionSnapshot> {
1495
- const plainRenderer = new PlainTranscriptRenderer()
1496
- const runStreamingCommandFn = options.runStreamingCommand ?? runStreamingCommand
1497
- const spawnStreamingCommandFn = options.spawnStreamingCommand ?? ((command: string[], spawnOptions?: RunStreamingCommandOptions): StreamingCommandHandle => ({
1498
- result: runStreamingCommandFn(command, spawnOptions),
1499
- interrupt() {},
1500
- kill() {},
1501
- }))
1502
-
1503
- const notifySnapshot = (snapshot: RunExecutionSnapshot) => {
1504
- options.onSnapshot?.(snapshot)
1505
- }
1506
-
1507
- notifySnapshot(tracker.snapshot())
1508
-
1509
- if (options.surface === 'plain') {
1510
- writeLine(options.writeStdout, 'Streaming Claude execution in one-shot mode.')
1511
- }
1512
-
1513
- let cancelled = false
1514
- const command = buildClaudeCommand(prepared, 'one-shot')
1515
-
1516
- const handle = spawnStreamingCommandFn(command, {
1517
- cwd: prepared.repoRoot,
1518
- env: buildClaudeRunEnvironment(),
1519
- allowFailure: true,
1520
- onStdoutLine: async (line) => {
1521
- const trimmed = line.trim()
1522
- if (!trimmed) {
1523
- return
1524
- }
1525
-
1526
- try {
1527
- const parsed = JSON.parse(trimmed) as Record<string, unknown>
1528
- const events = normalizeClaudePrintEvent(parsed)
1529
- for (const event of events) {
1530
- const snapshot = tracker.update(event)
1531
- notifySnapshot(snapshot)
1532
- if (options.surface === 'plain') {
1533
- for (const renderedLine of plainRenderer.render(event, snapshot, options.verbose)) {
1534
- writeLine(options.writeStdout, renderedLine)
1535
- }
1536
- }
1537
- }
1538
- }
1539
- catch {
1540
- if (options.surface === 'plain') {
1541
- writeLine(options.writeStdout, `Claude: ${trimmed}`)
1542
- }
1543
- }
1544
- },
1545
- onStderrLine: async (line) => {
1546
- const trimmed = line.trim()
1547
- if (!trimmed) {
1548
- return
1549
- }
1550
-
1551
- if (options.surface === 'plain') {
1552
- writeErrorLine(options.writeStderr, trimmed)
1553
- }
1554
- },
1555
- })
1556
-
1557
- options.onControl?.({
1558
- interrupt() {
1559
- cancelled = true
1560
- handle.interrupt()
1561
- },
1562
- kill() {
1563
- cancelled = true
1564
- handle.kill()
1565
- },
1566
- })
1567
-
1568
- const result = await handle.result
1569
- let snapshot = tracker.snapshot()
1570
-
1571
- if (result.exitCode !== 0) {
1572
- if (cancelled) {
1573
- snapshot = tracker.finalize('cancelled', {
1574
- summary: 'Run cancelled.',
1575
- })
1576
- notifySnapshot(snapshot)
1577
- throw buildRunCancelledError(snapshot)
1578
- }
1579
-
1580
- snapshot = tracker.finalize('failed')
1581
- notifySnapshot(snapshot)
1582
- throw buildRunFailureError(result, snapshot)
1583
- }
1584
-
1585
- if (snapshot.execution.status === 'running' || snapshot.execution.status === 'starting') {
1586
- snapshot = tracker.finalize('success')
1587
- notifySnapshot(snapshot)
1588
- }
1589
-
1590
- if (snapshot.execution.status === 'failed') {
1591
- throw buildRunFailureError(result, snapshot)
1592
- }
1593
-
1594
- return snapshot
1595
- }
1596
-
1597
- export async function runLocalAgentCommand(
1598
- options: RunLocalAgentCommandOptions = {},
1599
- ): Promise<RunCommandResult> {
1600
- const surface = options.surface ?? 'plain'
1601
- const effectiveOptions: RunLocalAgentCommandOptions = {
1602
- ...options,
1603
- surface,
1604
- }
1605
- const prepared = options.contract ?? await prepareLocalAgentRun({
1606
- cwd: options.cwd ?? process.cwd(),
1607
- commandExists: options.commandExists,
1608
- inspectClaudeCli: options.inspectClaudeCli,
1609
- runCommand: options.runCommand,
1610
- })
1611
- const mode: RunMode = prepared.config.isMultiturn ? 'chat' : 'one-shot'
1612
- const tracker = new RunStateTracker(prepared, mode)
1613
-
1614
- let snapshot = tracker.snapshot()
1615
- effectiveOptions.onSnapshot?.(snapshot)
1616
-
1617
- if (prepared.config.isMultiturn) {
1618
- await runInteractiveAgent(prepared, options.runInteractiveCommand ?? runInteractiveCommand, effectiveOptions)
1619
- snapshot = tracker.finalize('success', {
1620
- summary: 'Claude chat exited. Reading result artifact…',
1621
- })
1622
- effectiveOptions.onSnapshot?.(snapshot)
1623
- }
1624
- else {
1625
- snapshot = await runOneShotAgent(prepared, effectiveOptions, tracker)
1626
- }
1627
-
1628
- return await readResultArtifact(prepared, snapshot)
1629
- }
1630
-
1631
- function renderExecutionSteps(result: RunCommandResult, options: RenderOptions): string | undefined {
1632
- const recentSteps = result.steps.slice(-8).map(step => step.summary)
1633
- if (recentSteps.length === 0) {
1634
- return undefined
1635
- }
1636
-
1637
- return section('Recent steps', bulletList(recentSteps), options)
1638
- }
1639
-
1640
- export function renderRunExecutionRecap(
1641
- result: RunCommandResult,
1642
- options: RenderOptions = {},
1643
- ): string {
1644
- return resultCard('Execution recap', [
1645
- section('Execution', kvList([
1646
- { label: 'Repo root', value: result.repoRoot },
1647
- { label: 'Mode', value: result.mode },
1648
- { label: 'Model', value: result.config.model ?? '<default>' },
1649
- { label: 'Elapsed', value: formatDurationMs(result.execution.durationMs) },
1650
- { label: 'Turns', value: result.execution.numTurns != null ? String(result.execution.numTurns) : '<unknown>' },
1651
- { label: 'Status', value: result.execution.status },
1652
- { label: 'Stop reason', value: result.execution.stopReason ?? '<none>' },
1653
- { label: 'Cost', value: result.execution.totalCostUsd != null ? `$${result.execution.totalCostUsd.toFixed(4)}` : '<unknown>' },
1654
- {
1655
- label: 'maxRounds',
1656
- value: result.config.maxRounds != null
1657
- ? `${result.config.maxRounds} (ignored in local CLI)`
1658
- : '<unspecified>',
1659
- },
1660
- ], options), options),
1661
- result.execution.notableEvents.length > 0
1662
- ? section('Notable events', bulletList(result.execution.notableEvents), options)
1663
- : undefined,
1664
- result.execution.permissionDenials.length > 0
1665
- ? section('Permission denials', bulletList(result.execution.permissionDenials), options)
1666
- : undefined,
1667
- result.execution.errors.length > 0
1668
- ? section('Errors', bulletList(result.execution.errors), options)
1669
- : undefined,
1670
- renderExecutionSteps(result, options),
1671
- ], options)
1672
- }
1673
-
1674
- export function renderRunArtifactCard(
1675
- result: RunCommandResult,
1676
- options: RenderOptions = {},
1677
- ): string {
1678
- const resultContent = result.resultKind === 'json'
1679
- ? JSON.stringify(result.resultValue, null, 2)
1680
- : String(result.resultValue)
1681
-
1682
- return resultCard('Run result', [
1683
- section('Artifact', kvList([
1684
- { label: 'Path', value: result.resultPath },
1685
- { label: 'Kind', value: result.resultKind },
1686
- ], options), options),
1687
- section('Result', resultContent, options),
1688
- ], options)
1689
- }
1690
-
1691
- export function renderRunResult(
1692
- result: RunCommandResult,
1693
- options: RenderOptions = {},
1694
- ): string {
1695
- return joinBlocks([
1696
- renderRunExecutionRecap(result, options),
1697
- renderRunArtifactCard(result, options),
1698
- ])
1699
- }