@eve-horizon/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2266 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleJob = handleJob;
4
+ const child_process_1 = require("child_process");
5
+ const args_1 = require("../lib/args");
6
+ const client_1 = require("../lib/client");
7
+ const output_1 = require("../lib/output");
8
+ // ============================================================================
9
+ // Main Handler
10
+ // ============================================================================
11
+ async function handleJob(subcommand, positionals, flags, context) {
12
+ const json = Boolean(flags.json);
13
+ switch (subcommand) {
14
+ case 'create':
15
+ return handleCreate(flags, context, json);
16
+ case 'list':
17
+ return handleList(flags, context, json);
18
+ case 'ready':
19
+ return handleReady(flags, context, json);
20
+ case 'blocked':
21
+ return handleBlocked(flags, context, json);
22
+ case 'show':
23
+ return handleShow(positionals, flags, context, json);
24
+ case 'current':
25
+ return handleCurrent(positionals, flags, context, json);
26
+ case 'diagnose':
27
+ return handleDiagnose(positionals, context, json);
28
+ case 'tree':
29
+ return handleTree(positionals, context, json);
30
+ case 'update':
31
+ return handleUpdate(positionals, flags, context, json);
32
+ case 'close':
33
+ return handleClose(positionals, flags, context, json);
34
+ case 'cancel':
35
+ return handleCancel(positionals, flags, context, json);
36
+ case 'dep':
37
+ return handleDep(positionals, flags, context, json);
38
+ case 'claim':
39
+ return handleClaim(positionals, flags, context, json);
40
+ case 'release':
41
+ return handleRelease(positionals, flags, context, json);
42
+ case 'attempts':
43
+ return handleAttempts(positionals, context, json);
44
+ case 'logs':
45
+ return handleLogs(positionals, flags, context, json);
46
+ case 'submit':
47
+ return handleSubmit(positionals, flags, context, json);
48
+ case 'approve':
49
+ return handleApprove(positionals, flags, context, json);
50
+ case 'reject':
51
+ return handleReject(positionals, flags, context, json);
52
+ case 'result':
53
+ return handleResult(positionals, flags, context);
54
+ case 'follow':
55
+ return handleFollow(positionals, flags, context);
56
+ case 'wait':
57
+ return handleWait(positionals, flags, context);
58
+ case 'watch':
59
+ return handleWatch(positionals, flags, context);
60
+ case 'runner-logs':
61
+ return handleRunnerLogs(positionals, flags, context);
62
+ default:
63
+ throw new Error('Usage: eve jobs <create|list|ready|blocked|show|current|diagnose|tree|update|close|cancel|dep|claim|release|attempts|logs|submit|approve|reject|result|follow|wait|watch|runner-logs>');
64
+ }
65
+ }
66
+ // ============================================================================
67
+ // Subcommand Handlers
68
+ // ============================================================================
69
+ /**
70
+ * eve jobs create --project=X --description="..." [--title="..."] [--parent=X] [--type=task] [--priority=2] [--phase=ready] [--review=none|human|agent]
71
+ *
72
+ * --description is required (the work prompt sent to the harness)
73
+ * --title is optional (defaults to first 64 chars of description's first line)
74
+ *
75
+ * Scheduling hints (optional, used when scheduler claims the job):
76
+ * --harness=mclaude:fast Preferred harness (with optional :variant)
77
+ * --worker-type=default Worker type preference
78
+ * --permission=auto_edit Permission policy (default, auto_edit, yolo)
79
+ * --timeout=3600 Execution timeout in seconds
80
+ *
81
+ * Environment options:
82
+ * --env=<name> Environment name for persistent execution
83
+ * --execution-mode=<mode> Execution mode: 'persistent' or 'ephemeral' (default: persistent)
84
+ *
85
+ * Inline execution:
86
+ * --claim Create and immediately claim the job for execution
87
+ * --agent=<id> Agent ID for claim (default: $EVE_AGENT_ID or cli-user)
88
+ */
89
+ async function handleCreate(flags, context, json) {
90
+ const parentId = (0, args_1.getStringFlag)(flags, ['parent']);
91
+ const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
92
+ const description = (0, args_1.getStringFlag)(flags, ['description']);
93
+ let title = (0, args_1.getStringFlag)(flags, ['title']);
94
+ const issueType = (0, args_1.getStringFlag)(flags, ['type']) ?? 'task';
95
+ const priority = (0, args_1.getStringFlag)(flags, ['priority']);
96
+ const phase = (0, args_1.getStringFlag)(flags, ['phase']); // API defaults to 'ready'
97
+ const review = (0, args_1.getStringFlag)(flags, ['review']) ?? 'none';
98
+ const labels = (0, args_1.getStringFlag)(flags, ['labels']);
99
+ const assignee = (0, args_1.getStringFlag)(flags, ['assignee']);
100
+ const deferUntil = (0, args_1.getStringFlag)(flags, ['defer-until']);
101
+ const dueAt = (0, args_1.getStringFlag)(flags, ['due-at']);
102
+ // Scheduling hints
103
+ const harness = (0, args_1.getStringFlag)(flags, ['harness']);
104
+ const workerType = (0, args_1.getStringFlag)(flags, ['worker-type']);
105
+ const permission = (0, args_1.getStringFlag)(flags, ['permission']);
106
+ const timeout = (0, args_1.getStringFlag)(flags, ['timeout']);
107
+ // Environment options
108
+ const envName = (0, args_1.getStringFlag)(flags, ['env']);
109
+ const executionMode = (0, args_1.getStringFlag)(flags, ['execution-mode']) ?? 'persistent';
110
+ // Validate execution mode
111
+ if (executionMode !== 'persistent' && executionMode !== 'ephemeral') {
112
+ throw new Error('--execution-mode must be either "persistent" or "ephemeral"');
113
+ }
114
+ // Inline execution
115
+ const shouldClaim = Boolean(flags.claim);
116
+ const agentId = (0, args_1.getStringFlag)(flags, ['agent']) ?? process.env.EVE_AGENT_ID ?? 'cli-user';
117
+ // For root jobs, project is required. For child jobs, project is inherited from parent.
118
+ if (!parentId && !projectId) {
119
+ throw new Error('Usage: eve jobs create --project=<id> --description="..." [options]\n' +
120
+ ' eve jobs create --parent=<id> --description="..." [options]');
121
+ }
122
+ if (!description) {
123
+ throw new Error('--description is required (the work prompt)');
124
+ }
125
+ // Auto-generate title from first line of description if not provided
126
+ if (!title) {
127
+ const firstLine = description.split('\n')[0].trim();
128
+ title = firstLine.length > 64 ? firstLine.substring(0, 61) + '...' : firstLine;
129
+ }
130
+ const body = {
131
+ title,
132
+ description,
133
+ issue_type: issueType,
134
+ review_required: review,
135
+ execution_mode: executionMode,
136
+ };
137
+ // Add environment name (can be null)
138
+ body.env_name = envName ?? null;
139
+ if (parentId) {
140
+ body.parent_id = parentId;
141
+ }
142
+ if (phase) {
143
+ body.phase = phase;
144
+ }
145
+ if (priority !== undefined) {
146
+ body.priority = parseInt(priority, 10);
147
+ }
148
+ if (labels) {
149
+ body.labels = labels.split(',').map((l) => l.trim());
150
+ }
151
+ if (assignee) {
152
+ body.assignee = assignee;
153
+ }
154
+ if (deferUntil) {
155
+ body.defer_until = deferUntil;
156
+ }
157
+ if (dueAt) {
158
+ body.due_at = dueAt;
159
+ }
160
+ // Build hints object if any hint flags are provided
161
+ const hints = {};
162
+ if (harness)
163
+ hints.harness = harness;
164
+ if (workerType)
165
+ hints.worker_type = workerType;
166
+ if (permission)
167
+ hints.permission_policy = permission;
168
+ if (timeout)
169
+ hints.timeout_seconds = parseInt(timeout, 10);
170
+ if (Object.keys(hints).length > 0) {
171
+ body.hints = hints;
172
+ }
173
+ // Determine endpoint: root jobs use project endpoint, child jobs can use job-based endpoint
174
+ // For simplicity, always use project endpoint. If parent_id is set, the API inherits project from parent.
175
+ const resolvedProjectId = projectId ?? (parentId ? await getProjectFromJob(context, parentId) : undefined);
176
+ if (!resolvedProjectId) {
177
+ throw new Error('Could not determine project. Provide --project or ensure --parent exists.');
178
+ }
179
+ const response = await (0, client_1.requestJson)(context, `/projects/${resolvedProjectId}/jobs`, {
180
+ method: 'POST',
181
+ body,
182
+ });
183
+ // If --claim flag is set, immediately claim the job
184
+ if (shouldClaim) {
185
+ const claimBody = { agent_id: agentId };
186
+ // Use harness from hints if available
187
+ if (harness) {
188
+ claimBody.harness = harness;
189
+ }
190
+ const claimResponse = await (0, client_1.requestJson)(context, `/jobs/${response.id}/claim`, { method: 'POST', body: claimBody });
191
+ if (json) {
192
+ (0, output_1.outputJson)({ job: response, attempt: claimResponse.attempt }, json);
193
+ }
194
+ else {
195
+ console.log(`Created and claimed job: ${response.id}`);
196
+ console.log(` Title: ${response.title}`);
197
+ console.log(` Phase: active`);
198
+ console.log(` Priority: P${response.priority}`);
199
+ console.log(` Attempt: #${claimResponse.attempt.attempt_number}`);
200
+ console.log(` Agent: ${claimResponse.attempt.agent_id}`);
201
+ if (claimResponse.attempt.harness) {
202
+ console.log(` Harness: ${claimResponse.attempt.harness}`);
203
+ }
204
+ if (response.parent_id) {
205
+ console.log(` Parent: ${response.parent_id}`);
206
+ }
207
+ }
208
+ return;
209
+ }
210
+ if (json) {
211
+ (0, output_1.outputJson)(response, json);
212
+ }
213
+ else {
214
+ console.log(`Created job: ${response.id}`);
215
+ console.log(` Title: ${response.title}`);
216
+ console.log(` Phase: ${response.phase}`);
217
+ console.log(` Priority: P${response.priority}`);
218
+ if (response.parent_id) {
219
+ console.log(` Parent: ${response.parent_id}`);
220
+ }
221
+ if (Object.keys(hints).length > 0) {
222
+ console.log(` Hints: ${JSON.stringify(hints)}`);
223
+ }
224
+ }
225
+ }
226
+ /**
227
+ * Parse a relative time string (e.g., "1h", "30m", "2d") into an ISO timestamp
228
+ */
229
+ function parseSinceValue(since) {
230
+ // If it looks like an ISO date, return as-is
231
+ if (since.includes('T') || since.includes('-')) {
232
+ return since;
233
+ }
234
+ const match = since.match(/^(\d+)([mhd])$/);
235
+ if (!match) {
236
+ throw new Error(`Invalid --since format: "${since}". Use formats like "1h", "30m", "2d", or ISO timestamp.`);
237
+ }
238
+ const value = parseInt(match[1], 10);
239
+ const unit = match[2];
240
+ const now = new Date();
241
+ switch (unit) {
242
+ case 'm':
243
+ now.setMinutes(now.getMinutes() - value);
244
+ break;
245
+ case 'h':
246
+ now.setHours(now.getHours() - value);
247
+ break;
248
+ case 'd':
249
+ now.setDate(now.getDate() - value);
250
+ break;
251
+ }
252
+ return now.toISOString();
253
+ }
254
+ /**
255
+ * eve jobs list [--project=X] [--phase=ready] [--assignee=X] [--since=1h] [--stuck] [--limit=50] [--offset=0]
256
+ * eve jobs list --all [--org=X] [--project=X] [--phase=X] [--limit=50] [--offset=0]
257
+ */
258
+ async function handleList(flags, context, json) {
259
+ // Check for --all flag (admin mode - cross-project listing)
260
+ const all = Boolean(flags.all);
261
+ if (all) {
262
+ // Admin mode: list all jobs across projects
263
+ const query = buildQuery({
264
+ org_id: (0, args_1.getStringFlag)(flags, ['org']),
265
+ project_id: (0, args_1.getStringFlag)(flags, ['project']),
266
+ phase: (0, args_1.getStringFlag)(flags, ['phase']),
267
+ limit: (0, args_1.getStringFlag)(flags, ['limit']) ?? '50',
268
+ offset: (0, args_1.getStringFlag)(flags, ['offset']),
269
+ });
270
+ const response = await (0, client_1.requestJson)(context, `/jobs${query}`);
271
+ if (json) {
272
+ (0, output_1.outputJson)(response, json);
273
+ }
274
+ else {
275
+ if (response.jobs.length === 0) {
276
+ console.log('No jobs found.');
277
+ return;
278
+ }
279
+ console.log('All jobs (admin view):');
280
+ console.log('');
281
+ formatJobsTable(response.jobs);
282
+ }
283
+ return;
284
+ }
285
+ // Standard mode: project-scoped listing
286
+ const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
287
+ if (!projectId) {
288
+ throw new Error('Usage: eve jobs list --project=<id> [--phase=X] [--assignee=X] [--since=1h] [--stuck]\n eve jobs list --all [--org=X] [--project=X] [--phase=X]');
289
+ }
290
+ // Parse --since into ISO timestamp
291
+ const sinceRaw = (0, args_1.getStringFlag)(flags, ['since']);
292
+ const since = sinceRaw ? parseSinceValue(sinceRaw) : undefined;
293
+ // --stuck is a boolean flag
294
+ const stuck = Boolean(flags.stuck);
295
+ const stuckMinutes = (0, args_1.getStringFlag)(flags, ['stuck-minutes']);
296
+ const query = buildQuery({
297
+ phase: (0, args_1.getStringFlag)(flags, ['phase']),
298
+ assignee: (0, args_1.getStringFlag)(flags, ['assignee']),
299
+ priority: (0, args_1.getStringFlag)(flags, ['priority']),
300
+ since,
301
+ stuck: stuck ? 'true' : undefined,
302
+ stuck_minutes: stuckMinutes,
303
+ limit: (0, args_1.getStringFlag)(flags, ['limit']),
304
+ offset: (0, args_1.getStringFlag)(flags, ['offset']),
305
+ });
306
+ const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/jobs${query}`);
307
+ if (json) {
308
+ (0, output_1.outputJson)(response, json);
309
+ }
310
+ else {
311
+ if (stuck && response.jobs.length === 0) {
312
+ console.log('No stuck jobs found.');
313
+ return;
314
+ }
315
+ formatJobsTable(response.jobs);
316
+ }
317
+ }
318
+ /**
319
+ * eve jobs ready [--project=X] [--limit=10]
320
+ * Shortcut for showing schedulable jobs
321
+ */
322
+ async function handleReady(flags, context, json) {
323
+ const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
324
+ if (!projectId) {
325
+ throw new Error('Usage: eve jobs ready --project=<id> [--limit=10]');
326
+ }
327
+ const query = buildQuery({
328
+ limit: (0, args_1.getStringFlag)(flags, ['limit']) ?? '10',
329
+ });
330
+ const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/jobs/ready${query}`);
331
+ if (json) {
332
+ (0, output_1.outputJson)(response, json);
333
+ }
334
+ else {
335
+ if (response.jobs.length === 0) {
336
+ console.log('No ready jobs found.');
337
+ return;
338
+ }
339
+ console.log('Ready jobs (schedulable):');
340
+ console.log('');
341
+ formatJobsTable(response.jobs);
342
+ }
343
+ }
344
+ /**
345
+ * eve jobs blocked [--project=X]
346
+ * Show jobs that are blocked by dependencies
347
+ */
348
+ async function handleBlocked(flags, context, json) {
349
+ const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
350
+ if (!projectId) {
351
+ throw new Error('Usage: eve jobs blocked --project=<id>');
352
+ }
353
+ const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/jobs/blocked`);
354
+ if (json) {
355
+ (0, output_1.outputJson)(response, json);
356
+ }
357
+ else {
358
+ if (response.jobs.length === 0) {
359
+ console.log('No blocked jobs found.');
360
+ return;
361
+ }
362
+ console.log('Blocked jobs (waiting on dependencies):');
363
+ console.log('');
364
+ formatJobsTable(response.jobs);
365
+ }
366
+ }
367
+ /**
368
+ * eve jobs show <id>
369
+ */
370
+ async function handleShow(positionals, flags, context, json) {
371
+ const jobId = positionals[0];
372
+ const verbose = Boolean(flags.verbose || flags.v);
373
+ if (!jobId) {
374
+ throw new Error('Usage: eve jobs show <job-id> [--verbose]');
375
+ }
376
+ const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
377
+ // Fetch attempts if verbose
378
+ let attempts = [];
379
+ let latestAttempt;
380
+ if (verbose) {
381
+ const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
382
+ attempts = attemptsResponse.attempts || [];
383
+ // Get result details for the latest attempt
384
+ if (attempts.length > 0) {
385
+ const latest = attempts[attempts.length - 1];
386
+ try {
387
+ const result = await (0, client_1.requestJson)(context, `/jobs/${jobId}/result?attempt=${latest.attempt_number}`);
388
+ latestAttempt = { ...latest, result };
389
+ }
390
+ catch {
391
+ latestAttempt = { ...latest };
392
+ }
393
+ }
394
+ }
395
+ if (json) {
396
+ (0, output_1.outputJson)(verbose ? { ...job, attempts, latestAttempt } : job, json);
397
+ }
398
+ else {
399
+ formatJobDetails(job);
400
+ if (verbose && attempts.length > 0) {
401
+ formatAttemptsVerbose(attempts, latestAttempt);
402
+ }
403
+ }
404
+ }
405
+ /**
406
+ * eve jobs current [<job-id>] [--json|--tree]
407
+ * Defaults to EVE_JOB_ID when present
408
+ */
409
+ async function handleCurrent(positionals, flags, context, json) {
410
+ const jobId = positionals[0] ?? process.env.EVE_JOB_ID;
411
+ const tree = (0, args_1.getBooleanFlag)(flags, ['tree']) ?? false;
412
+ if (!jobId) {
413
+ throw new Error('Usage: eve job current [<job-id>] [--json|--tree]');
414
+ }
415
+ if (tree) {
416
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/tree`);
417
+ if (json) {
418
+ (0, output_1.outputJson)(response, json);
419
+ }
420
+ else {
421
+ formatJobTree(response, 0);
422
+ }
423
+ return;
424
+ }
425
+ const response = await requestJobContext(context, jobId);
426
+ (0, output_1.outputJson)(response, true);
427
+ }
428
+ /**
429
+ * eve jobs diagnose <id>
430
+ * Comprehensive job debugging - shows job state, attempts, timeline, logs, and recommendations
431
+ */
432
+ async function handleDiagnose(positionals, context, json) {
433
+ const jobId = positionals[0];
434
+ if (!jobId) {
435
+ throw new Error('Usage: eve jobs diagnose <job-id>');
436
+ }
437
+ // Fetch job details
438
+ const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
439
+ // Fetch attempts
440
+ const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
441
+ const attempts = attemptsResponse.attempts || [];
442
+ // Fetch latest result
443
+ let latestResult = null;
444
+ if (attempts.length > 0) {
445
+ const latest = attempts[attempts.length - 1];
446
+ try {
447
+ latestResult = await (0, client_1.requestJson)(context, `/jobs/${jobId}/result?attempt=${latest.attempt_number}`);
448
+ }
449
+ catch {
450
+ // No result available
451
+ }
452
+ }
453
+ // Fetch recent logs (if we have attempts)
454
+ let logs = [];
455
+ if (attempts.length > 0) {
456
+ const latest = attempts[attempts.length - 1];
457
+ try {
458
+ const logsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts/${latest.attempt_number}/logs?limit=50`);
459
+ logs = logsResponse.logs || [];
460
+ }
461
+ catch {
462
+ // No logs available
463
+ }
464
+ }
465
+ if (json) {
466
+ (0, output_1.outputJson)({ job, attempts, latestResult, logs }, json);
467
+ }
468
+ else {
469
+ formatDiagnose(job, attempts, latestResult, logs);
470
+ }
471
+ }
472
+ /**
473
+ * eve jobs tree <id>
474
+ * Show job hierarchy
475
+ */
476
+ async function handleTree(positionals, context, json) {
477
+ const jobId = positionals[0];
478
+ if (!jobId) {
479
+ throw new Error('Usage: eve jobs tree <job-id>');
480
+ }
481
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/tree`);
482
+ if (json) {
483
+ (0, output_1.outputJson)(response, json);
484
+ }
485
+ else {
486
+ formatJobTree(response, 0);
487
+ }
488
+ }
489
+ /**
490
+ * eve jobs update <id> --phase=X --priority=X --assignee=X
491
+ */
492
+ async function handleUpdate(positionals, flags, context, json) {
493
+ const jobId = positionals[0];
494
+ if (!jobId) {
495
+ throw new Error('Usage: eve jobs update <job-id> [--phase=X] [--priority=X] [--assignee=X]');
496
+ }
497
+ const body = {};
498
+ const phase = (0, args_1.getStringFlag)(flags, ['phase']);
499
+ const priority = (0, args_1.getStringFlag)(flags, ['priority']);
500
+ const assignee = (0, args_1.getStringFlag)(flags, ['assignee']);
501
+ const title = (0, args_1.getStringFlag)(flags, ['title']);
502
+ const description = (0, args_1.getStringFlag)(flags, ['description']);
503
+ const labels = (0, args_1.getStringFlag)(flags, ['labels']);
504
+ const deferUntil = (0, args_1.getStringFlag)(flags, ['defer-until']);
505
+ const dueAt = (0, args_1.getStringFlag)(flags, ['due-at']);
506
+ const review = (0, args_1.getStringFlag)(flags, ['review']);
507
+ if (phase)
508
+ body.phase = phase;
509
+ if (priority !== undefined)
510
+ body.priority = parseInt(priority, 10);
511
+ if (assignee)
512
+ body.assignee = assignee;
513
+ if (title)
514
+ body.title = title;
515
+ if (description)
516
+ body.description = description;
517
+ if (labels)
518
+ body.labels = labels.split(',').map((l) => l.trim());
519
+ if (deferUntil)
520
+ body.defer_until = deferUntil;
521
+ if (dueAt)
522
+ body.due_at = dueAt;
523
+ if (review)
524
+ body.review_required = review;
525
+ if (Object.keys(body).length === 0) {
526
+ throw new Error('No updates provided. Use --phase, --priority, --assignee, etc.');
527
+ }
528
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}`, {
529
+ method: 'PATCH',
530
+ body,
531
+ });
532
+ if (json) {
533
+ (0, output_1.outputJson)(response, json);
534
+ }
535
+ else {
536
+ console.log(`Updated job: ${response.id}`);
537
+ console.log(` Phase: ${response.phase}`);
538
+ console.log(` Priority: P${response.priority}`);
539
+ if (response.assignee) {
540
+ console.log(` Assignee: ${response.assignee}`);
541
+ }
542
+ }
543
+ }
544
+ /**
545
+ * eve jobs close <id> [--reason="..."]
546
+ * Mark job as done
547
+ */
548
+ async function handleClose(positionals, flags, context, json) {
549
+ const jobId = positionals[0];
550
+ if (!jobId) {
551
+ throw new Error('Usage: eve jobs close <job-id> [--reason="..."]');
552
+ }
553
+ const reason = (0, args_1.getStringFlag)(flags, ['reason']);
554
+ const body = {
555
+ phase: 'done',
556
+ };
557
+ if (reason) {
558
+ body.close_reason = reason;
559
+ }
560
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}`, {
561
+ method: 'PATCH',
562
+ body,
563
+ });
564
+ if (json) {
565
+ (0, output_1.outputJson)(response, json);
566
+ }
567
+ else {
568
+ console.log(`Closed job: ${response.id}`);
569
+ console.log(` Phase: ${response.phase}`);
570
+ if (response.close_reason) {
571
+ console.log(` Reason: ${response.close_reason}`);
572
+ }
573
+ }
574
+ }
575
+ /**
576
+ * eve jobs cancel <id> [--reason="..."]
577
+ * Mark job as cancelled
578
+ */
579
+ async function handleCancel(positionals, flags, context, json) {
580
+ const jobId = positionals[0];
581
+ if (!jobId) {
582
+ throw new Error('Usage: eve jobs cancel <job-id> [--reason="..."]');
583
+ }
584
+ const reason = (0, args_1.getStringFlag)(flags, ['reason']);
585
+ const body = {
586
+ phase: 'cancelled',
587
+ };
588
+ if (reason) {
589
+ body.close_reason = reason;
590
+ }
591
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}`, {
592
+ method: 'PATCH',
593
+ body,
594
+ });
595
+ if (json) {
596
+ (0, output_1.outputJson)(response, json);
597
+ }
598
+ else {
599
+ console.log(`Cancelled job: ${response.id}`);
600
+ console.log(` Phase: ${response.phase}`);
601
+ if (response.close_reason) {
602
+ console.log(` Reason: ${response.close_reason}`);
603
+ }
604
+ }
605
+ }
606
+ /**
607
+ * eve jobs dep <add|remove|list> [args]
608
+ * Manage job dependencies
609
+ */
610
+ async function handleDep(positionals, flags, context, json) {
611
+ const depSubcommand = positionals[0];
612
+ switch (depSubcommand) {
613
+ case 'add':
614
+ return handleDepAdd(positionals.slice(1), flags, context, json);
615
+ case 'remove':
616
+ return handleDepRemove(positionals.slice(1), context, json);
617
+ case 'list':
618
+ return handleDepList(positionals.slice(1), context, json);
619
+ default:
620
+ throw new Error('Usage: eve jobs dep <add|remove|list> [args]');
621
+ }
622
+ }
623
+ /**
624
+ * eve jobs dep add <from> <to> [--type=blocks]
625
+ * Add dependency: "from depends on to" (to blocks from)
626
+ */
627
+ async function handleDepAdd(positionals, flags, context, json) {
628
+ const fromId = positionals[0];
629
+ const toId = positionals[1];
630
+ if (!fromId || !toId) {
631
+ throw new Error('Usage: eve jobs dep add <from> <to> [--type=blocks]');
632
+ }
633
+ const relationType = (0, args_1.getStringFlag)(flags, ['type']) ?? 'blocks';
634
+ const response = await (0, client_1.requestJson)(context, `/jobs/${fromId}/dependencies`, {
635
+ method: 'POST',
636
+ body: {
637
+ related_job_id: toId,
638
+ relation_type: relationType,
639
+ },
640
+ });
641
+ if (json) {
642
+ (0, output_1.outputJson)(response, json);
643
+ }
644
+ else {
645
+ console.log(`Added dependency: ${fromId} depends on ${toId} (${relationType})`);
646
+ }
647
+ }
648
+ /**
649
+ * eve jobs dep remove <from> <to>
650
+ * Remove dependency
651
+ */
652
+ async function handleDepRemove(positionals, context, json) {
653
+ const fromId = positionals[0];
654
+ const toId = positionals[1];
655
+ if (!fromId || !toId) {
656
+ throw new Error('Usage: eve jobs dep remove <from> <to>');
657
+ }
658
+ const response = await (0, client_1.requestJson)(context, `/jobs/${fromId}/dependencies/${toId}`, {
659
+ method: 'DELETE',
660
+ });
661
+ if (json) {
662
+ (0, output_1.outputJson)(response, json);
663
+ }
664
+ else {
665
+ console.log(`Removed dependency: ${fromId} no longer depends on ${toId}`);
666
+ }
667
+ }
668
+ /**
669
+ * eve jobs dep list <id>
670
+ * Show dependencies and dependents for a job
671
+ */
672
+ async function handleDepList(positionals, context, json) {
673
+ const jobId = positionals[0];
674
+ if (!jobId) {
675
+ throw new Error('Usage: eve jobs dep list <job-id>');
676
+ }
677
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/dependencies`);
678
+ if (json) {
679
+ (0, output_1.outputJson)(response, json);
680
+ }
681
+ else {
682
+ formatDependencies(response);
683
+ }
684
+ }
685
+ // ============================================================================
686
+ // Helper Functions
687
+ // ============================================================================
688
+ async function requestJobContext(context, jobId) {
689
+ const response = await (0, client_1.requestRaw)(context, `/jobs/${jobId}/context`);
690
+ if (response.ok) {
691
+ return response.data;
692
+ }
693
+ if (response.status !== 404) {
694
+ const message = typeof response.data === 'string' ? response.data : response.text;
695
+ throw new Error(`HTTP ${response.status}: ${message}`);
696
+ }
697
+ const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
698
+ return { job };
699
+ }
700
+ function buildQuery(params) {
701
+ const search = new URLSearchParams();
702
+ Object.entries(params).forEach(([key, value]) => {
703
+ if (value === undefined || value === '')
704
+ return;
705
+ search.set(key, String(value));
706
+ });
707
+ const query = search.toString();
708
+ return query ? `?${query}` : '';
709
+ }
710
+ /**
711
+ * Get project ID from an existing job (used when creating child jobs)
712
+ */
713
+ async function getProjectFromJob(context, jobId) {
714
+ try {
715
+ const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
716
+ return job.project_id;
717
+ }
718
+ catch {
719
+ return undefined;
720
+ }
721
+ }
722
+ /**
723
+ * Format jobs as a human-readable table
724
+ */
725
+ function formatJobsTable(jobs) {
726
+ if (jobs.length === 0) {
727
+ console.log('No jobs found.');
728
+ return;
729
+ }
730
+ // Calculate column widths
731
+ const idWidth = Math.max(4, ...jobs.map((j) => j.id.length));
732
+ const phaseWidth = Math.max(5, ...jobs.map((j) => j.phase.length));
733
+ const titleWidth = Math.min(50, Math.max(5, ...jobs.map((j) => j.title.length)));
734
+ // Header
735
+ const header = [
736
+ padRight('ID', idWidth),
737
+ padRight('P', 2),
738
+ padRight('Phase', phaseWidth),
739
+ padRight('Type', 8),
740
+ padRight('Title', titleWidth),
741
+ 'Assignee',
742
+ ].join(' ');
743
+ console.log(header);
744
+ console.log('-'.repeat(header.length));
745
+ // Rows
746
+ for (const job of jobs) {
747
+ const title = job.title.length > titleWidth ? job.title.slice(0, titleWidth - 3) + '...' : job.title;
748
+ const row = [
749
+ padRight(job.id, idWidth),
750
+ padRight(`P${job.priority}`, 2),
751
+ padRight(job.phase, phaseWidth),
752
+ padRight(job.issue_type, 8),
753
+ padRight(title, titleWidth),
754
+ job.assignee ?? '-',
755
+ ].join(' ');
756
+ console.log(row);
757
+ }
758
+ console.log('');
759
+ console.log(`Total: ${jobs.length} job(s)`);
760
+ }
761
+ /**
762
+ * Format a single job's details
763
+ */
764
+ function formatJobDetails(job) {
765
+ console.log(`Job: ${job.id}`);
766
+ console.log('');
767
+ console.log(` Title: ${job.title}`);
768
+ console.log(` Phase: ${job.phase}`);
769
+ console.log(` Priority: P${job.priority}`);
770
+ console.log(` Type: ${job.issue_type}`);
771
+ console.log(` Project: ${job.project_id}`);
772
+ if (job.parent_id) {
773
+ console.log(` Parent: ${job.parent_id}`);
774
+ }
775
+ console.log(` Depth: ${job.depth}`);
776
+ if (job.description) {
777
+ console.log(` Description: ${job.description}`);
778
+ }
779
+ if (job.labels && job.labels.length > 0) {
780
+ console.log(` Labels: ${job.labels.join(', ')}`);
781
+ }
782
+ if (job.assignee) {
783
+ console.log(` Assignee: ${job.assignee}`);
784
+ }
785
+ console.log(` Review: ${job.review_required}`);
786
+ if (job.review_status) {
787
+ console.log(` Review Status: ${job.review_status}`);
788
+ }
789
+ if (job.reviewer) {
790
+ console.log(` Reviewer: ${job.reviewer}`);
791
+ }
792
+ if (job.defer_until) {
793
+ console.log(` Defer Until: ${job.defer_until}`);
794
+ }
795
+ if (job.due_at) {
796
+ console.log(` Due At: ${job.due_at}`);
797
+ }
798
+ console.log('');
799
+ console.log(` Created: ${formatDate(job.created_at)}`);
800
+ console.log(` Updated: ${formatDate(job.updated_at)}`);
801
+ if (job.closed_at) {
802
+ console.log(` Closed: ${formatDate(job.closed_at)}`);
803
+ }
804
+ if (job.close_reason) {
805
+ console.log(` Close Reason: ${job.close_reason}`);
806
+ }
807
+ }
808
+ /**
809
+ * Format attempts with verbose details (for --verbose flag)
810
+ */
811
+ function formatAttemptsVerbose(attempts, latestAttempt) {
812
+ console.log('');
813
+ console.log('Attempts:');
814
+ for (const attempt of attempts) {
815
+ const isLatest = latestAttempt && attempt.id === latestAttempt.id;
816
+ const statusIcon = attempt.status === 'succeeded' ? '✓' :
817
+ attempt.status === 'failed' ? '✗' :
818
+ attempt.status === 'running' ? '▶' : '○';
819
+ console.log(` ${statusIcon} Attempt #${attempt.attempt_number} (${attempt.status})`);
820
+ console.log(` ID: ${attempt.id}`);
821
+ console.log(` Started: ${formatDate(attempt.started_at)}`);
822
+ if (attempt.ended_at) {
823
+ console.log(` Ended: ${formatDate(attempt.ended_at)}`);
824
+ }
825
+ if (attempt.harness) {
826
+ console.log(` Harness: ${attempt.harness}`);
827
+ }
828
+ if (attempt.agent_id) {
829
+ console.log(` Agent: ${attempt.agent_id}`);
830
+ }
831
+ // Show result details for latest attempt
832
+ if (isLatest && latestAttempt?.result) {
833
+ const r = latestAttempt.result;
834
+ if (r.exitCode !== null) {
835
+ console.log(` Exit: ${r.exitCode}`);
836
+ }
837
+ if (r.durationMs !== null) {
838
+ console.log(` Duration: ${(r.durationMs / 1000).toFixed(1)}s`);
839
+ }
840
+ if (r.errorMessage) {
841
+ console.log(` Error: ${r.errorMessage}`);
842
+ }
843
+ if (r.tokenInput || r.tokenOutput) {
844
+ console.log(` Tokens: ${r.tokenInput || 0} in / ${r.tokenOutput || 0} out`);
845
+ }
846
+ }
847
+ console.log('');
848
+ }
849
+ }
850
+ /**
851
+ * Format comprehensive diagnostic output
852
+ */
853
+ function formatDiagnose(job, attempts, latestResult, logs) {
854
+ // Header
855
+ console.log('╭────────────────────────────────────────────────────────────────╮');
856
+ console.log(`│ Job Diagnosis: ${job.id.padEnd(47)} │`);
857
+ console.log('╰────────────────────────────────────────────────────────────────╯');
858
+ console.log('');
859
+ // Status summary
860
+ // Note: 'failed' is not a valid job phase - failures are 'cancelled' with a close_reason
861
+ const statusIcon = job.phase === 'done' ? '✓' :
862
+ job.phase === 'cancelled' ? '⊘' :
863
+ job.phase === 'active' ? '▶' : '○';
864
+ console.log(`Status: ${statusIcon} ${job.phase.toUpperCase()}`);
865
+ console.log(` Priority: P${job.priority}`);
866
+ console.log(` Assignee: ${job.assignee || '(none)'}`);
867
+ console.log(` Created: ${formatDate(job.created_at)}`);
868
+ console.log(` Updated: ${formatDate(job.updated_at)}`);
869
+ console.log('');
870
+ // Title and description
871
+ console.log(`Title: ${job.title}`);
872
+ if (job.description && job.description !== job.title) {
873
+ const desc = job.description.length > 200
874
+ ? job.description.substring(0, 200) + '...'
875
+ : job.description;
876
+ console.log(`Description: ${desc}`);
877
+ }
878
+ console.log('');
879
+ // Attempts timeline
880
+ console.log('Timeline:');
881
+ console.log(` ${formatDate(job.created_at)} - Job created (phase: ready)`);
882
+ for (const attempt of attempts) {
883
+ const statusIcon = attempt.status === 'succeeded' ? '✓' :
884
+ attempt.status === 'failed' ? '✗' :
885
+ attempt.status === 'running' ? '▶' : '○';
886
+ console.log(` ${formatDate(attempt.started_at)} - Attempt #${attempt.attempt_number} started`);
887
+ if (attempt.ended_at) {
888
+ console.log(` ${formatDate(attempt.ended_at)} - Attempt #${attempt.attempt_number} ${statusIcon} ${attempt.status}`);
889
+ }
890
+ }
891
+ if (job.closed_at) {
892
+ console.log(` ${formatDate(job.closed_at)} - Job closed (${job.phase})`);
893
+ }
894
+ console.log('');
895
+ // Lifecycle timeline from logs
896
+ const lifecycleLogs = logs.filter(l => l.type?.startsWith('lifecycle_'));
897
+ if (lifecycleLogs.length > 0) {
898
+ console.log('Execution Timeline:');
899
+ for (const log of lifecycleLogs) {
900
+ const content = log.line;
901
+ const phase = content?.phase || 'unknown';
902
+ const action = content?.action || '';
903
+ const duration = content?.duration_ms;
904
+ const success = content?.success;
905
+ const error = content?.error;
906
+ if (action === 'end') {
907
+ const durationStr = duration ? `${duration}ms` : '';
908
+ const statusIcon = success === false ? '✗' : '✓';
909
+ const errorStr = error ? ` - ${error.split('\n')[0]}` : '';
910
+ console.log(` ${statusIcon} ${phase}: ${durationStr}${errorStr}`);
911
+ }
912
+ }
913
+ console.log('');
914
+ }
915
+ // Latest attempt details
916
+ if (attempts.length > 0) {
917
+ const latest = attempts[attempts.length - 1];
918
+ console.log(`Latest Attempt (#${latest.attempt_number}):`);
919
+ console.log(` Status: ${latest.status}`);
920
+ console.log(` Harness: ${latest.harness || '(default)'}`);
921
+ console.log(` Agent: ${latest.agent_id || '(none)'}`);
922
+ if (latestResult) {
923
+ if (latestResult.exitCode !== null) {
924
+ console.log(` Exit Code: ${latestResult.exitCode}`);
925
+ }
926
+ if (latestResult.durationMs !== null) {
927
+ console.log(` Duration: ${(latestResult.durationMs / 1000).toFixed(1)}s`);
928
+ }
929
+ if (latestResult.errorMessage) {
930
+ console.log('');
931
+ console.log('Error Message:');
932
+ console.log(` ${latestResult.errorMessage}`);
933
+ }
934
+ }
935
+ console.log('');
936
+ }
937
+ // Recent logs
938
+ if (logs.length > 0) {
939
+ console.log('Recent Logs:');
940
+ const recentLogs = logs.slice(-10);
941
+ for (const log of recentLogs) {
942
+ const time = formatDate(log.timestamp);
943
+ let msg = '';
944
+ try {
945
+ msg = log.line != null
946
+ ? JSON.stringify(log.line).substring(0, 100)
947
+ : '(empty)';
948
+ }
949
+ catch {
950
+ msg = '(error formatting log)';
951
+ }
952
+ console.log(` ${time} ${msg}`);
953
+ }
954
+ console.log('');
955
+ }
956
+ // Diagnosis / recommendations
957
+ console.log('Diagnosis:');
958
+ if (job.phase === 'done') {
959
+ console.log(' ✓ Job completed successfully');
960
+ }
961
+ else if (job.phase === 'cancelled') {
962
+ // Cancelled jobs may have a failure reason in close_reason or attempt error
963
+ if (latestResult?.errorMessage?.includes('git clone')) {
964
+ console.log(' ✗ Git clone failed - check repo URL and credentials');
965
+ console.log(' Hint: Ensure GITHUB_TOKEN is set in project/org secrets');
966
+ }
967
+ else if (latestResult?.errorMessage?.includes('Service')) {
968
+ console.log(' ✗ Service provisioning failed');
969
+ console.log(' Hint: Check .eve/services.yaml and container logs');
970
+ }
971
+ else if (latestResult?.errorMessage) {
972
+ console.log(` ⊘ Cancelled: ${latestResult.errorMessage}`);
973
+ }
974
+ else if (job.close_reason) {
975
+ console.log(` ⊘ Cancelled: ${job.close_reason}`);
976
+ }
977
+ else {
978
+ console.log(' ⊘ Job was cancelled');
979
+ }
980
+ }
981
+ else if (job.phase === 'active' && attempts.length > 0) {
982
+ const latest = attempts[attempts.length - 1];
983
+ if (latest.status === 'running') {
984
+ const startedAt = new Date(latest.started_at).getTime();
985
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
986
+ if (elapsed > 300) {
987
+ console.log(` ⚠ Attempt running for ${elapsed}s - may be stuck`);
988
+ console.log(' Hint: Check orchestrator/worker logs');
989
+ }
990
+ else {
991
+ console.log(` ▶ Attempt in progress (${elapsed}s elapsed)`);
992
+ }
993
+ }
994
+ }
995
+ else if (job.phase === 'ready') {
996
+ console.log(' ○ Job is ready, waiting to be claimed');
997
+ if (job.assignee) {
998
+ console.log(` Assigned to: ${job.assignee}`);
999
+ }
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Format job hierarchy as a tree
1004
+ */
1005
+ function formatJobTree(node, indent) {
1006
+ const prefix = indent === 0 ? '' : ' '.repeat(indent - 1) + (indent > 0 ? '|- ' : '');
1007
+ const phaseIcon = getPhaseIcon(node.phase);
1008
+ const line = `${prefix}${phaseIcon} ${node.id} [P${node.priority}] ${node.title}`;
1009
+ console.log(line);
1010
+ if (node.children && node.children.length > 0) {
1011
+ for (const child of node.children) {
1012
+ formatJobTree(child, indent + 1);
1013
+ }
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Get an ASCII icon for a phase
1018
+ */
1019
+ function getPhaseIcon(phase) {
1020
+ switch (phase) {
1021
+ case 'idea':
1022
+ return '[?]';
1023
+ case 'backlog':
1024
+ return '[ ]';
1025
+ case 'ready':
1026
+ return '[>]';
1027
+ case 'active':
1028
+ return '[*]';
1029
+ case 'review':
1030
+ return '[R]';
1031
+ case 'done':
1032
+ return '[x]';
1033
+ case 'cancelled':
1034
+ return '[-]';
1035
+ default:
1036
+ return '[ ]';
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Format a date string for display
1041
+ */
1042
+ function formatDate(dateStr) {
1043
+ try {
1044
+ const date = new Date(dateStr);
1045
+ return date.toLocaleString();
1046
+ }
1047
+ catch {
1048
+ return dateStr;
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Format dependencies output
1053
+ */
1054
+ function formatDependencies(response) {
1055
+ console.log('');
1056
+ // Dependencies (jobs this one depends on)
1057
+ if (response.dependencies.length > 0) {
1058
+ console.log('Dependencies (this job depends on):');
1059
+ for (const dep of response.dependencies) {
1060
+ const phaseIcon = getPhaseIcon(dep.phase);
1061
+ const isBlocking = response.blocking.some((b) => b.id === dep.id);
1062
+ const blockingWarning = isBlocking ? ' ⚠️ BLOCKING' : '';
1063
+ const relationType = dep.relation_type !== 'blocks' ? ` (${dep.relation_type})` : '';
1064
+ console.log(` ${dep.id} ${phaseIcon} "${dep.title}"${relationType}${blockingWarning}`);
1065
+ }
1066
+ }
1067
+ else {
1068
+ console.log('Dependencies (this job depends on): None');
1069
+ }
1070
+ console.log('');
1071
+ // Dependents (jobs that depend on this one)
1072
+ if (response.dependents.length > 0) {
1073
+ console.log('Dependents (jobs depending on this):');
1074
+ for (const dependent of response.dependents) {
1075
+ const phaseIcon = getPhaseIcon(dependent.phase);
1076
+ const relationType = dependent.relation_type !== 'blocks' ? ` (${dependent.relation_type})` : '';
1077
+ console.log(` ${dependent.id} ${phaseIcon} "${dependent.title}"${relationType}`);
1078
+ }
1079
+ }
1080
+ else {
1081
+ console.log('Dependents (jobs depending on this): None');
1082
+ }
1083
+ console.log('');
1084
+ }
1085
+ /**
1086
+ * eve jobs claim <id> [--agent=X] [--harness=X]
1087
+ * Claims a job, creating an attempt and transitioning to active phase
1088
+ */
1089
+ async function handleClaim(positionals, flags, context, json) {
1090
+ const jobId = positionals[0];
1091
+ if (!jobId) {
1092
+ throw new Error('Usage: eve jobs claim <job-id> [--agent=X] [--harness=X]');
1093
+ }
1094
+ const agentId = (0, args_1.getStringFlag)(flags, ['agent']) ?? 'cli-user';
1095
+ const harness = (0, args_1.getStringFlag)(flags, ['harness']);
1096
+ const body = {
1097
+ agent_id: agentId,
1098
+ };
1099
+ if (harness) {
1100
+ body.harness = harness;
1101
+ }
1102
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/claim`, {
1103
+ method: 'POST',
1104
+ body,
1105
+ });
1106
+ if (json) {
1107
+ (0, output_1.outputJson)(response, json);
1108
+ }
1109
+ else {
1110
+ console.log(`Claimed job: ${jobId}`);
1111
+ console.log('');
1112
+ console.log(` Attempt: #${response.attempt.attempt_number}`);
1113
+ console.log(` Attempt ID: ${response.attempt.id}`);
1114
+ console.log(` Status: ${response.attempt.status}`);
1115
+ console.log(` Agent: ${response.attempt.agent_id}`);
1116
+ if (response.attempt.harness) {
1117
+ console.log(` Harness: ${response.attempt.harness}`);
1118
+ }
1119
+ console.log(` Started: ${formatDate(response.attempt.started_at)}`);
1120
+ }
1121
+ }
1122
+ /**
1123
+ * eve jobs release <id> [--agent=X] [--reason="..."]
1124
+ * Releases a job, ending the current attempt and setting back to ready
1125
+ */
1126
+ async function handleRelease(positionals, flags, context, json) {
1127
+ const jobId = positionals[0];
1128
+ if (!jobId) {
1129
+ throw new Error('Usage: eve jobs release <job-id> [--agent=X] [--reason="..."]');
1130
+ }
1131
+ const agentId = (0, args_1.getStringFlag)(flags, ['agent']) ?? 'cli-user';
1132
+ const reason = (0, args_1.getStringFlag)(flags, ['reason']);
1133
+ const body = {
1134
+ agent_id: agentId,
1135
+ };
1136
+ if (reason) {
1137
+ body.reason = reason;
1138
+ }
1139
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/release`, {
1140
+ method: 'POST',
1141
+ body,
1142
+ });
1143
+ if (json) {
1144
+ (0, output_1.outputJson)(response, json);
1145
+ }
1146
+ else {
1147
+ console.log(`Released job: ${jobId}`);
1148
+ console.log('');
1149
+ console.log(` Phase: ${response.job.phase}`);
1150
+ console.log(` Assignee: ${response.job.assignee ?? '(none)'}`);
1151
+ if (reason) {
1152
+ console.log(` Reason: ${reason}`);
1153
+ }
1154
+ }
1155
+ }
1156
+ /**
1157
+ * eve jobs attempts <id>
1158
+ * Lists all attempts for a job
1159
+ */
1160
+ async function handleAttempts(positionals, context, json) {
1161
+ const jobId = positionals[0];
1162
+ if (!jobId) {
1163
+ throw new Error('Usage: eve jobs attempts <job-id>');
1164
+ }
1165
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
1166
+ if (json) {
1167
+ (0, output_1.outputJson)(response, json);
1168
+ }
1169
+ else {
1170
+ if (response.attempts.length === 0) {
1171
+ console.log('No attempts found for this job.');
1172
+ return;
1173
+ }
1174
+ console.log(`Attempts for job: ${jobId}`);
1175
+ console.log('');
1176
+ for (const attempt of response.attempts) {
1177
+ const statusIcon = getStatusIcon(attempt.status);
1178
+ const duration = attempt.ended_at
1179
+ ? formatDuration(attempt.started_at, attempt.ended_at)
1180
+ : 'running';
1181
+ console.log(`${statusIcon} Attempt #${attempt.attempt_number} (${attempt.id})`);
1182
+ console.log(` Status: ${attempt.status}`);
1183
+ console.log(` Trigger: ${attempt.trigger_type}`);
1184
+ if (attempt.agent_id) {
1185
+ console.log(` Agent: ${attempt.agent_id}`);
1186
+ }
1187
+ if (attempt.harness) {
1188
+ console.log(` Harness: ${attempt.harness}`);
1189
+ }
1190
+ console.log(` Started: ${formatDate(attempt.started_at)}`);
1191
+ if (attempt.ended_at) {
1192
+ console.log(` Ended: ${formatDate(attempt.ended_at)}`);
1193
+ }
1194
+ console.log(` Duration: ${duration}`);
1195
+ if (attempt.result_summary) {
1196
+ console.log(` Summary: ${attempt.result_summary}`);
1197
+ }
1198
+ console.log('');
1199
+ }
1200
+ console.log(`Total: ${response.attempts.length} attempt(s)`);
1201
+ }
1202
+ }
1203
+ /**
1204
+ * eve jobs logs <id> [--attempt=N] [--after=N] [--follow]
1205
+ * View execution logs for a job attempt
1206
+ */
1207
+ async function handleLogs(positionals, flags, context, json) {
1208
+ const jobId = positionals[0];
1209
+ if (!jobId) {
1210
+ throw new Error('Usage: eve jobs logs <job-id> [--attempt=N] [--after=N]');
1211
+ }
1212
+ // Get attempt number (default to latest)
1213
+ const attemptStr = (0, args_1.getStringFlag)(flags, ['attempt']);
1214
+ let attemptNum;
1215
+ if (attemptStr) {
1216
+ attemptNum = parseInt(attemptStr, 10);
1217
+ }
1218
+ else {
1219
+ // Find the latest attempt
1220
+ const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
1221
+ if (attemptsResponse.attempts.length === 0) {
1222
+ console.log('No attempts found for this job.');
1223
+ return;
1224
+ }
1225
+ attemptNum = Math.max(...attemptsResponse.attempts.map(a => a.attempt_number));
1226
+ }
1227
+ const afterStr = (0, args_1.getStringFlag)(flags, ['after']);
1228
+ const afterQuery = afterStr ? `?after=${afterStr}` : '';
1229
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts/${attemptNum}/logs${afterQuery}`);
1230
+ if (json) {
1231
+ (0, output_1.outputJson)(response, json);
1232
+ }
1233
+ else {
1234
+ if (response.logs.length === 0) {
1235
+ console.log(`No logs found for attempt #${attemptNum}.`);
1236
+ return;
1237
+ }
1238
+ console.log(`Logs for job ${jobId}, attempt #${attemptNum}:`);
1239
+ console.log('');
1240
+ for (const log of response.logs) {
1241
+ formatLogEntry(log);
1242
+ }
1243
+ console.log('');
1244
+ console.log(`Total: ${response.logs.length} log entries`);
1245
+ }
1246
+ }
1247
+ /**
1248
+ * Format a single log entry for display
1249
+ */
1250
+ function formatLogEntry(log) {
1251
+ const line = log.line;
1252
+ const timestamp = new Date(log.timestamp).toLocaleTimeString();
1253
+ // Common log line formats from harnesses
1254
+ // Type can be at top level (from API) or inside line (from content)
1255
+ const type = log.type || line.type || 'log';
1256
+ // If type starts with 'lifecycle_', format as lifecycle event
1257
+ if (type.startsWith('lifecycle_')) {
1258
+ const content = line;
1259
+ const phase = content.phase || 'unknown';
1260
+ const action = content.action || 'unknown';
1261
+ const duration = content.duration_ms;
1262
+ const success = content.success;
1263
+ const error = content.error;
1264
+ const meta = content.meta || {};
1265
+ if (action === 'start') {
1266
+ const detail = formatLifecycleMeta(phase, meta);
1267
+ console.log(`[${timestamp}] ${getLifecycleIcon(phase)} Starting ${phase}${detail}...`);
1268
+ }
1269
+ else if (action === 'end') {
1270
+ const durationStr = duration ? ` (${duration}ms)` : '';
1271
+ if (success === false && error) {
1272
+ console.log(`[${timestamp}] ${getLifecycleIcon(phase)} ${capitalize(phase)} failed${durationStr}: ${error}`);
1273
+ }
1274
+ else {
1275
+ console.log(`[${timestamp}] ${getLifecycleIcon(phase)} ${capitalize(phase)} completed${durationStr}`);
1276
+ }
1277
+ }
1278
+ else if (action === 'log') {
1279
+ const msg = meta.message || JSON.stringify(meta);
1280
+ console.log(`[${timestamp}] > ${msg}`);
1281
+ }
1282
+ return;
1283
+ }
1284
+ const message = line.message || line.text || '';
1285
+ const tool = line.tool;
1286
+ const toolInput = line.tool_input;
1287
+ const toolResult = line.tool_result;
1288
+ // Format based on log type
1289
+ switch (type) {
1290
+ case 'assistant':
1291
+ case 'text':
1292
+ console.log(`[${timestamp}] 🤖 ${message || JSON.stringify(line)}`);
1293
+ break;
1294
+ case 'tool_use':
1295
+ console.log(`[${timestamp}] 🔧 ${tool || 'tool'}: ${toolInput || JSON.stringify(line)}`);
1296
+ break;
1297
+ case 'tool_result':
1298
+ const resultPreview = (toolResult || '').substring(0, 100);
1299
+ console.log(`[${timestamp}] → ${resultPreview}${(toolResult?.length || 0) > 100 ? '...' : ''}`);
1300
+ break;
1301
+ case 'error':
1302
+ console.log(`[${timestamp}] ❌ ${message || JSON.stringify(line)}`);
1303
+ break;
1304
+ case 'status':
1305
+ console.log(`[${timestamp}] ℹ️ ${message || JSON.stringify(line)}`);
1306
+ break;
1307
+ default:
1308
+ // Generic JSON output for unknown types
1309
+ console.log(`[${timestamp}] ${JSON.stringify(line)}`);
1310
+ }
1311
+ }
1312
+ /**
1313
+ * Get icon for lifecycle phase
1314
+ */
1315
+ function getLifecycleIcon(phase) {
1316
+ switch (phase) {
1317
+ case 'workspace': return '📁';
1318
+ case 'hook': return '🪝';
1319
+ case 'secrets': return '🔐';
1320
+ case 'services': return '🐳';
1321
+ case 'harness': return '🤖';
1322
+ case 'runner': return '☸️';
1323
+ default: return '⚙️';
1324
+ }
1325
+ }
1326
+ /**
1327
+ * Capitalize first letter of string
1328
+ */
1329
+ function capitalize(str) {
1330
+ return str.charAt(0).toUpperCase() + str.slice(1);
1331
+ }
1332
+ /**
1333
+ * Format lifecycle metadata for display
1334
+ */
1335
+ function formatLifecycleMeta(phase, meta) {
1336
+ switch (phase) {
1337
+ case 'workspace':
1338
+ const repoUrl = meta.repo_url || '';
1339
+ const branch = meta.branch || '';
1340
+ return branch ? ` (${repoUrl}@${branch})` : repoUrl ? ` (${repoUrl})` : '';
1341
+ case 'hook':
1342
+ return meta.hook_name ? ` "${meta.hook_name}"` : '';
1343
+ case 'secrets':
1344
+ return '';
1345
+ case 'services':
1346
+ const svcName = meta.service_name || '';
1347
+ return svcName ? ` "${svcName}"` : '';
1348
+ case 'harness':
1349
+ return meta.harness ? ` ${meta.harness}` : '';
1350
+ case 'runner':
1351
+ return meta.pod_name ? ` (${meta.pod_name})` : '';
1352
+ default:
1353
+ return '';
1354
+ }
1355
+ }
1356
+ /**
1357
+ * eve jobs submit <id> --summary="..."
1358
+ * Submit a job for review
1359
+ */
1360
+ async function handleSubmit(positionals, flags, context, json) {
1361
+ const jobId = positionals[0];
1362
+ if (!jobId) {
1363
+ throw new Error('Usage: eve jobs submit <job-id> --summary="..."');
1364
+ }
1365
+ const summary = (0, args_1.getStringFlag)(flags, ['summary']);
1366
+ if (!summary) {
1367
+ throw new Error('--summary is required');
1368
+ }
1369
+ // Use a default agent ID if not provided (could be from context in future)
1370
+ const agentId = (0, args_1.getStringFlag)(flags, ['agent-id']) ?? 'cli-user';
1371
+ const body = {
1372
+ agent_id: agentId,
1373
+ summary,
1374
+ };
1375
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/submit`, {
1376
+ method: 'POST',
1377
+ body,
1378
+ });
1379
+ if (json) {
1380
+ (0, output_1.outputJson)(response, json);
1381
+ }
1382
+ else {
1383
+ console.log(`Submitted job for review: ${response.id}`);
1384
+ console.log(` Phase: ${response.phase}`);
1385
+ console.log(` Review Status: ${response.review_status ?? 'N/A'}`);
1386
+ console.log(` Summary: ${summary}`);
1387
+ }
1388
+ }
1389
+ /**
1390
+ * eve jobs approve <id> [--comment="..."]
1391
+ * Approve a job in review
1392
+ */
1393
+ async function handleApprove(positionals, flags, context, json) {
1394
+ const jobId = positionals[0];
1395
+ if (!jobId) {
1396
+ throw new Error('Usage: eve jobs approve <job-id> [--comment="..."]');
1397
+ }
1398
+ const comment = (0, args_1.getStringFlag)(flags, ['comment']);
1399
+ const reviewerId = (0, args_1.getStringFlag)(flags, ['reviewer-id']) ?? 'cli-user';
1400
+ const body = {
1401
+ reviewer_id: reviewerId,
1402
+ };
1403
+ if (comment) {
1404
+ body.comment = comment;
1405
+ }
1406
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/approve`, {
1407
+ method: 'POST',
1408
+ body,
1409
+ });
1410
+ if (json) {
1411
+ (0, output_1.outputJson)(response, json);
1412
+ }
1413
+ else {
1414
+ console.log(`Approved job: ${response.id}`);
1415
+ console.log(` Phase: ${response.phase}`);
1416
+ console.log(` Review Status: ${response.review_status ?? 'N/A'}`);
1417
+ console.log(` Reviewer: ${response.reviewer ?? 'N/A'}`);
1418
+ if (comment) {
1419
+ console.log(` Comment: ${comment}`);
1420
+ }
1421
+ }
1422
+ }
1423
+ /**
1424
+ * eve jobs reject <id> --reason="..."
1425
+ * Reject a job in review
1426
+ */
1427
+ async function handleReject(positionals, flags, context, json) {
1428
+ const jobId = positionals[0];
1429
+ if (!jobId) {
1430
+ throw new Error('Usage: eve jobs reject <job-id> --reason="..."');
1431
+ }
1432
+ const reason = (0, args_1.getStringFlag)(flags, ['reason']);
1433
+ if (!reason) {
1434
+ throw new Error('--reason is required');
1435
+ }
1436
+ const reviewerId = (0, args_1.getStringFlag)(flags, ['reviewer-id']) ?? 'cli-user';
1437
+ const body = {
1438
+ reviewer_id: reviewerId,
1439
+ reason,
1440
+ };
1441
+ const response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/reject`, {
1442
+ method: 'POST',
1443
+ body,
1444
+ });
1445
+ if (json) {
1446
+ (0, output_1.outputJson)(response, json);
1447
+ }
1448
+ else {
1449
+ console.log(`Rejected job: ${response.id}`);
1450
+ console.log(` Phase: ${response.phase}`);
1451
+ console.log(` Review Status: ${response.review_status ?? 'N/A'}`);
1452
+ console.log(` Reviewer: ${response.reviewer ?? 'N/A'}`);
1453
+ console.log(` Reason: ${reason}`);
1454
+ console.log('');
1455
+ console.log('A new attempt has been created automatically for retry.');
1456
+ }
1457
+ }
1458
+ /**
1459
+ * eve jobs result <id> [--format=text|json|full] [--attempt=N]
1460
+ * Fetch and display the result from a completed job attempt
1461
+ */
1462
+ async function handleResult(positionals, flags, context) {
1463
+ const jobId = positionals[0];
1464
+ if (!jobId) {
1465
+ throw new Error('Usage: eve jobs result <job-id> [--format=text|json|full] [--attempt=N]');
1466
+ }
1467
+ const format = (0, args_1.getStringFlag)(flags, ['format']) ?? 'text';
1468
+ const attemptStr = (0, args_1.getStringFlag)(flags, ['attempt']);
1469
+ // Validate format
1470
+ if (!['text', 'json', 'full'].includes(format)) {
1471
+ throw new Error('Invalid format. Use --format=text|json|full');
1472
+ }
1473
+ // Build query params
1474
+ const query = attemptStr ? `?attempt=${attemptStr}` : '';
1475
+ let response;
1476
+ try {
1477
+ response = await (0, client_1.requestJson)(context, `/jobs/${jobId}/result${query}`);
1478
+ }
1479
+ catch (error) {
1480
+ const err = error;
1481
+ // Check for specific error cases
1482
+ if (err.statusCode === 404) {
1483
+ console.error(`Error: Job '${jobId}' not found.`);
1484
+ process.exit(1);
1485
+ }
1486
+ throw error;
1487
+ }
1488
+ // Handle job still running
1489
+ if (response.status === 'running' || response.status === 'pending') {
1490
+ console.error(`Error: Job is still ${response.status}. Use 'eve job wait ${jobId}' to wait for completion.`);
1491
+ process.exit(1);
1492
+ }
1493
+ // Handle failed job
1494
+ if (response.status === 'failed') {
1495
+ console.error(`Job failed (attempt #${response.attemptNumber}):`);
1496
+ if (response.errorMessage) {
1497
+ console.error(` ${response.errorMessage}`);
1498
+ }
1499
+ else {
1500
+ console.error(' No error message available.');
1501
+ }
1502
+ process.exit(1);
1503
+ }
1504
+ // Format output based on --format flag
1505
+ switch (format) {
1506
+ case 'text':
1507
+ formatResultText(response);
1508
+ break;
1509
+ case 'json':
1510
+ formatResultJson(response);
1511
+ break;
1512
+ case 'full':
1513
+ formatResultFull(response);
1514
+ break;
1515
+ }
1516
+ }
1517
+ /**
1518
+ * Format result as plain text (default)
1519
+ */
1520
+ function formatResultText(response) {
1521
+ if (response.resultText) {
1522
+ console.log(response.resultText);
1523
+ }
1524
+ else {
1525
+ console.log('(no result text)');
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Format result as JSON
1530
+ */
1531
+ function formatResultJson(response) {
1532
+ const output = {
1533
+ success: response.status === 'succeeded',
1534
+ exitCode: response.exitCode,
1535
+ resultText: response.resultText,
1536
+ resultJson: response.resultJson,
1537
+ durationMs: response.durationMs,
1538
+ tokenUsage: response.tokenInput !== null || response.tokenOutput !== null
1539
+ ? { input: response.tokenInput, output: response.tokenOutput }
1540
+ : null,
1541
+ };
1542
+ console.log(JSON.stringify(output, null, 2));
1543
+ }
1544
+ /**
1545
+ * Format result with full details
1546
+ */
1547
+ function formatResultFull(response) {
1548
+ console.log(`Job: ${response.jobId}`);
1549
+ console.log(`Attempt: ${response.attemptNumber}`);
1550
+ console.log(`Status: ${response.status}`);
1551
+ if (response.durationMs !== null) {
1552
+ const durationSec = Math.round(response.durationMs / 1000);
1553
+ console.log(`Duration: ${durationSec}s`);
1554
+ }
1555
+ if (response.tokenInput !== null || response.tokenOutput !== null) {
1556
+ const inputStr = response.tokenInput !== null ? formatNumber(response.tokenInput) : '0';
1557
+ const outputStr = response.tokenOutput !== null ? formatNumber(response.tokenOutput) : '0';
1558
+ console.log(`Tokens: ${inputStr} in / ${outputStr} out`);
1559
+ }
1560
+ console.log('');
1561
+ console.log('Result:');
1562
+ if (response.resultText) {
1563
+ console.log(response.resultText);
1564
+ }
1565
+ else {
1566
+ console.log('(no result text)');
1567
+ }
1568
+ }
1569
+ /**
1570
+ * Format a number with commas for readability
1571
+ */
1572
+ function formatNumber(num) {
1573
+ return num.toLocaleString();
1574
+ }
1575
+ // Exit codes for wait command
1576
+ const EXIT_CODE_TIMEOUT = 124;
1577
+ const EXIT_CODE_CANCELLED = 125;
1578
+ /**
1579
+ * eve job wait <id> [--timeout=300] [--quiet] [--verbose] [--json]
1580
+ * Wait for a job to complete, polling the wait endpoint
1581
+ */
1582
+ async function handleWait(positionals, flags, context) {
1583
+ const jobId = positionals[0];
1584
+ if (!jobId) {
1585
+ throw new Error('Usage: eve job wait <job-id> [--timeout=300] [--quiet] [--verbose] [--json]');
1586
+ }
1587
+ const maxTimeout = parseInt((0, args_1.getStringFlag)(flags, ['timeout']) ?? '300', 10);
1588
+ const quiet = Boolean(flags.quiet);
1589
+ const verbose = Boolean(flags.verbose);
1590
+ const json = Boolean(flags.json);
1591
+ const startTime = Date.now();
1592
+ let totalElapsed = 0;
1593
+ // Track state changes for verbose mode
1594
+ let lastPhase;
1595
+ let lastStatus;
1596
+ let lastPeriodicUpdate = 0;
1597
+ const periodicUpdateInterval = 5; // seconds
1598
+ // Progress output
1599
+ if (!quiet && !json) {
1600
+ if (verbose) {
1601
+ console.log(`[0s] Waiting for job ${jobId}...`);
1602
+ }
1603
+ else {
1604
+ console.log(`Waiting for ${jobId}...`);
1605
+ }
1606
+ }
1607
+ // Poll with increasing timeout values to reduce request count
1608
+ // Start with 5s, increase to 30s, cap at 60s per poll
1609
+ let pollTimeout = 5;
1610
+ while (totalElapsed < maxTimeout) {
1611
+ // Calculate remaining time and cap poll timeout (must be integer for API)
1612
+ const remainingTime = maxTimeout - totalElapsed;
1613
+ const currentPollTimeout = Math.floor(Math.min(pollTimeout, remainingTime, 60));
1614
+ const response = await (0, client_1.requestRaw)(context, `/jobs/${jobId}/wait?timeout=${currentPollTimeout}`);
1615
+ if (response.status === 200) {
1616
+ // Job completed - response.data is JobResultResponse
1617
+ const result = response.data;
1618
+ const totalDuration = Math.round((Date.now() - startTime) / 1000);
1619
+ // Show error message immediately in verbose mode if job failed
1620
+ if (verbose && !quiet && !json && result.status === 'failed' && result.errorMessage) {
1621
+ console.log(`[${totalDuration}s] Job failed: ${result.errorMessage}`);
1622
+ }
1623
+ if (json) {
1624
+ (0, output_1.outputJson)({
1625
+ jobId: result.jobId,
1626
+ status: result.status,
1627
+ exitCode: result.exitCode,
1628
+ durationMs: result.durationMs,
1629
+ resultText: result.resultText,
1630
+ resultJson: result.resultJson,
1631
+ waitDurationSeconds: totalDuration,
1632
+ }, true);
1633
+ }
1634
+ else if (!quiet) {
1635
+ if (verbose) {
1636
+ console.log(`[${totalDuration}s] Completed`);
1637
+ }
1638
+ else {
1639
+ console.log(`[+] Completed in ${totalDuration}s`);
1640
+ }
1641
+ console.log('');
1642
+ if (result.resultText) {
1643
+ console.log(result.resultText);
1644
+ }
1645
+ else if (result.resultJson) {
1646
+ console.log(JSON.stringify(result.resultJson, null, 2));
1647
+ }
1648
+ }
1649
+ // Exit with job's exit code or based on status
1650
+ if (result.status === 'succeeded') {
1651
+ process.exit(result.exitCode ?? 0);
1652
+ }
1653
+ else if (result.status === 'failed') {
1654
+ process.exit(result.exitCode ?? 1);
1655
+ }
1656
+ else if (result.status === 'cancelled') {
1657
+ process.exit(EXIT_CODE_CANCELLED);
1658
+ }
1659
+ // Default exit code
1660
+ process.exit(result.exitCode ?? 0);
1661
+ }
1662
+ else if (response.status === 202) {
1663
+ // Job still running - continue polling
1664
+ const timeoutResponse = response.data;
1665
+ totalElapsed = Math.round((Date.now() - startTime) / 1000);
1666
+ if (!quiet && !json) {
1667
+ if (verbose) {
1668
+ // Check for phase changes
1669
+ if (lastPhase !== undefined && lastPhase !== timeoutResponse.phase) {
1670
+ console.log(`[${totalElapsed}s] Phase: ${lastPhase} → ${timeoutResponse.phase}`);
1671
+ }
1672
+ lastPhase = timeoutResponse.phase;
1673
+ // Check for status changes
1674
+ if (lastStatus !== undefined && lastStatus !== timeoutResponse.status) {
1675
+ console.log(`[${totalElapsed}s] Status: ${lastStatus} → ${timeoutResponse.status}`);
1676
+ }
1677
+ lastStatus = timeoutResponse.status;
1678
+ // Show periodic elapsed time updates
1679
+ if (totalElapsed - lastPeriodicUpdate >= periodicUpdateInterval) {
1680
+ console.log(`[${totalElapsed}s] Still waiting... (phase: ${timeoutResponse.phase})`);
1681
+ lastPeriodicUpdate = totalElapsed;
1682
+ }
1683
+ }
1684
+ else {
1685
+ process.stdout.write(`\rWaiting for ${jobId}... (${totalElapsed}s, ${timeoutResponse.status})`);
1686
+ }
1687
+ }
1688
+ // Increase poll timeout for next iteration (with cap)
1689
+ pollTimeout = Math.min(pollTimeout * 1.5, 60);
1690
+ }
1691
+ else if (response.status === 404) {
1692
+ console.error(`Error: Job '${jobId}' not found.`);
1693
+ process.exit(1);
1694
+ }
1695
+ else {
1696
+ // Unexpected error
1697
+ const message = typeof response.data === 'string' ? response.data : response.text;
1698
+ throw new Error(`HTTP ${response.status}: ${message}`);
1699
+ }
1700
+ }
1701
+ // Overall timeout reached
1702
+ if (!quiet && !json) {
1703
+ console.log('');
1704
+ }
1705
+ if (json) {
1706
+ (0, output_1.outputJson)({
1707
+ jobId,
1708
+ status: 'timeout',
1709
+ message: `Timeout after ${maxTimeout}s`,
1710
+ waitDurationSeconds: maxTimeout,
1711
+ }, true);
1712
+ }
1713
+ else {
1714
+ console.error(`Timeout: Job did not complete within ${maxTimeout}s.`);
1715
+ }
1716
+ process.exit(EXIT_CODE_TIMEOUT);
1717
+ }
1718
+ /**
1719
+ * eve job watch <id> [--timeout=300]
1720
+ * Watch a job by combining status polling + log streaming
1721
+ * Shows phase/status changes like `wait --verbose` while streaming logs like `follow`
1722
+ */
1723
+ async function handleWatch(positionals, flags, context) {
1724
+ const jobId = positionals[0];
1725
+ if (!jobId) {
1726
+ throw new Error('Usage: eve job watch <job-id> [--timeout=300]');
1727
+ }
1728
+ const maxTimeout = parseInt((0, args_1.getStringFlag)(flags, ['timeout']) ?? '300', 10);
1729
+ console.log(`Watching job ${jobId}...`);
1730
+ const startTime = Date.now();
1731
+ let lastPhase;
1732
+ let lastStatus;
1733
+ // Set up SSE log streaming
1734
+ const url = `${context.apiUrl}/jobs/${jobId}/stream`;
1735
+ const headers = {
1736
+ Accept: 'text/event-stream',
1737
+ };
1738
+ if (context.token) {
1739
+ headers.Authorization = `Bearer ${context.token}`;
1740
+ }
1741
+ let exitCode = 0;
1742
+ let streamEnded = false;
1743
+ let jobCompleted = false;
1744
+ // Start status polling in background
1745
+ const statusPollingPromise = (async () => {
1746
+ while (!jobCompleted && !streamEnded) {
1747
+ await new Promise(resolve => setTimeout(resolve, 3000)); // Poll every 3 seconds
1748
+ if (jobCompleted || streamEnded)
1749
+ break;
1750
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
1751
+ // Check timeout
1752
+ if (elapsed >= maxTimeout) {
1753
+ console.log('');
1754
+ console.error(`Timeout: Job did not complete within ${maxTimeout}s.`);
1755
+ process.exit(EXIT_CODE_TIMEOUT);
1756
+ }
1757
+ try {
1758
+ // Quick status check
1759
+ const job = await (0, client_1.requestJson)(context, `/jobs/${jobId}`);
1760
+ // Track phase changes
1761
+ if (lastPhase !== undefined && lastPhase !== job.phase) {
1762
+ console.log(`[${elapsed}s] Phase: ${lastPhase} → ${job.phase}`);
1763
+ }
1764
+ lastPhase = job.phase;
1765
+ // Check if job reached terminal state
1766
+ // Note: 'failed' is not a valid job phase - only 'done' and 'cancelled' are terminal
1767
+ if (job.phase === 'done' || job.phase === 'cancelled') {
1768
+ jobCompleted = true;
1769
+ break;
1770
+ }
1771
+ // Get latest attempt to track status changes
1772
+ const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
1773
+ if (attemptsResponse.attempts.length > 0) {
1774
+ const latest = attemptsResponse.attempts[attemptsResponse.attempts.length - 1];
1775
+ if (lastStatus !== undefined && lastStatus !== latest.status) {
1776
+ console.log(`[${elapsed}s] Attempt status: ${lastStatus} → ${latest.status}`);
1777
+ }
1778
+ lastStatus = latest.status;
1779
+ }
1780
+ }
1781
+ catch (error) {
1782
+ // Ignore errors during polling, rely on SSE stream
1783
+ }
1784
+ }
1785
+ })();
1786
+ // Start SSE log streaming
1787
+ try {
1788
+ const response = await fetch(url, { headers });
1789
+ if (!response.ok) {
1790
+ const text = await response.text();
1791
+ throw new Error(`HTTP ${response.status}: ${text}`);
1792
+ }
1793
+ if (!response.body) {
1794
+ throw new Error('No response body received');
1795
+ }
1796
+ const reader = response.body.getReader();
1797
+ const decoder = new TextDecoder();
1798
+ let buffer = '';
1799
+ while (true) {
1800
+ const { done, value } = await reader.read();
1801
+ if (done) {
1802
+ break;
1803
+ }
1804
+ buffer += decoder.decode(value, { stream: true });
1805
+ // Process complete SSE events from buffer
1806
+ const lines = buffer.split('\n');
1807
+ buffer = lines.pop() ?? ''; // Keep incomplete line in buffer
1808
+ let eventType = '';
1809
+ let eventData = '';
1810
+ for (const line of lines) {
1811
+ if (line.startsWith('event:')) {
1812
+ eventType = line.slice(6).trim();
1813
+ }
1814
+ else if (line.startsWith('data:')) {
1815
+ eventData = line.slice(5).trim();
1816
+ }
1817
+ else if (line === '' && eventData) {
1818
+ // Empty line marks end of event
1819
+ exitCode = processWatchSSEEvent(eventType, eventData);
1820
+ if (exitCode !== -1) {
1821
+ // Event signals we should exit
1822
+ streamEnded = true;
1823
+ jobCompleted = true;
1824
+ await statusPollingPromise; // Wait for polling to finish
1825
+ process.exit(exitCode);
1826
+ }
1827
+ eventType = '';
1828
+ eventData = '';
1829
+ }
1830
+ }
1831
+ }
1832
+ // Process any remaining data
1833
+ if (buffer.trim()) {
1834
+ const remainingLines = buffer.split('\n');
1835
+ let eventType = '';
1836
+ let eventData = '';
1837
+ for (const line of remainingLines) {
1838
+ if (line.startsWith('event:')) {
1839
+ eventType = line.slice(6).trim();
1840
+ }
1841
+ else if (line.startsWith('data:')) {
1842
+ eventData = line.slice(5).trim();
1843
+ }
1844
+ }
1845
+ if (eventData) {
1846
+ exitCode = processWatchSSEEvent(eventType, eventData);
1847
+ if (exitCode !== -1) {
1848
+ streamEnded = true;
1849
+ jobCompleted = true;
1850
+ await statusPollingPromise;
1851
+ process.exit(exitCode);
1852
+ }
1853
+ }
1854
+ }
1855
+ // Stream ended
1856
+ streamEnded = true;
1857
+ jobCompleted = true;
1858
+ await statusPollingPromise;
1859
+ const totalDuration = Math.round((Date.now() - startTime) / 1000);
1860
+ console.log('');
1861
+ console.log(`[${totalDuration}s] Stream ended.`);
1862
+ process.exit(exitCode);
1863
+ }
1864
+ catch (error) {
1865
+ streamEnded = true;
1866
+ jobCompleted = true;
1867
+ await statusPollingPromise;
1868
+ const err = error;
1869
+ console.error(`Error watching job: ${err.message}`);
1870
+ process.exit(1);
1871
+ }
1872
+ }
1873
+ /**
1874
+ * Process a single SSE event for watch command
1875
+ * Returns exit code if we should exit, or -1 to continue
1876
+ */
1877
+ function processWatchSSEEvent(eventType, eventData) {
1878
+ let parsed;
1879
+ try {
1880
+ parsed = JSON.parse(eventData);
1881
+ }
1882
+ catch {
1883
+ // If we can't parse, ignore it
1884
+ return -1;
1885
+ }
1886
+ // Handle different event types
1887
+ switch (eventType) {
1888
+ case 'log': {
1889
+ const logEvent = parsed;
1890
+ formatFollowLogLine(logEvent);
1891
+ return -1;
1892
+ }
1893
+ case 'complete': {
1894
+ const completeEvent = parsed;
1895
+ console.log('');
1896
+ console.log('[+] Completed');
1897
+ if (completeEvent.result) {
1898
+ console.log('');
1899
+ console.log(completeEvent.result);
1900
+ }
1901
+ return completeEvent.exit_code ?? 0;
1902
+ }
1903
+ case 'error': {
1904
+ const errorEvent = parsed;
1905
+ console.log('');
1906
+ console.log('[!] Error');
1907
+ if (errorEvent.error) {
1908
+ console.log(` ${errorEvent.error}`);
1909
+ }
1910
+ return errorEvent.exit_code ?? 1;
1911
+ }
1912
+ default:
1913
+ return -1;
1914
+ }
1915
+ }
1916
+ /**
1917
+ * eve job follow <id> [--raw] [--no-result]
1918
+ * Stream logs from a running job in real-time via SSE
1919
+ */
1920
+ async function handleFollow(positionals, flags, context) {
1921
+ const jobId = positionals[0];
1922
+ if (!jobId) {
1923
+ throw new Error('Usage: eve job follow <job-id> [--raw] [--no-result]');
1924
+ }
1925
+ const raw = Boolean(flags.raw);
1926
+ const showResult = !flags['no-result'];
1927
+ console.log(`Following ${jobId}...`);
1928
+ const url = `${context.apiUrl}/jobs/${jobId}/stream`;
1929
+ const headers = {
1930
+ Accept: 'text/event-stream',
1931
+ };
1932
+ if (context.token) {
1933
+ headers.Authorization = `Bearer ${context.token}`;
1934
+ }
1935
+ let exitCode = 0;
1936
+ try {
1937
+ const response = await fetch(url, { headers });
1938
+ if (!response.ok) {
1939
+ const text = await response.text();
1940
+ throw new Error(`HTTP ${response.status}: ${text}`);
1941
+ }
1942
+ if (!response.body) {
1943
+ throw new Error('No response body received');
1944
+ }
1945
+ const reader = response.body.getReader();
1946
+ const decoder = new TextDecoder();
1947
+ let buffer = '';
1948
+ while (true) {
1949
+ const { done, value } = await reader.read();
1950
+ if (done) {
1951
+ break;
1952
+ }
1953
+ buffer += decoder.decode(value, { stream: true });
1954
+ // Process complete SSE events from buffer
1955
+ const lines = buffer.split('\n');
1956
+ buffer = lines.pop() ?? ''; // Keep incomplete line in buffer
1957
+ let eventType = '';
1958
+ let eventData = '';
1959
+ for (const line of lines) {
1960
+ if (line.startsWith('event:')) {
1961
+ eventType = line.slice(6).trim();
1962
+ }
1963
+ else if (line.startsWith('data:')) {
1964
+ eventData = line.slice(5).trim();
1965
+ }
1966
+ else if (line === '' && eventData) {
1967
+ // Empty line marks end of event
1968
+ exitCode = processSSEEvent(eventType, eventData, raw, showResult);
1969
+ if (exitCode !== -1) {
1970
+ // Event signals we should exit
1971
+ process.exit(exitCode);
1972
+ }
1973
+ eventType = '';
1974
+ eventData = '';
1975
+ }
1976
+ }
1977
+ }
1978
+ // Process any remaining data
1979
+ if (buffer.trim()) {
1980
+ const remainingLines = buffer.split('\n');
1981
+ let eventType = '';
1982
+ let eventData = '';
1983
+ for (const line of remainingLines) {
1984
+ if (line.startsWith('event:')) {
1985
+ eventType = line.slice(6).trim();
1986
+ }
1987
+ else if (line.startsWith('data:')) {
1988
+ eventData = line.slice(5).trim();
1989
+ }
1990
+ }
1991
+ if (eventData) {
1992
+ exitCode = processSSEEvent(eventType, eventData, raw, showResult);
1993
+ if (exitCode !== -1) {
1994
+ process.exit(exitCode);
1995
+ }
1996
+ }
1997
+ }
1998
+ // Stream ended without explicit complete/error event
1999
+ console.log('');
2000
+ console.log('Stream ended.');
2001
+ process.exit(0);
2002
+ }
2003
+ catch (error) {
2004
+ const err = error;
2005
+ console.error(`Error following job: ${err.message}`);
2006
+ process.exit(1);
2007
+ }
2008
+ }
2009
+ /**
2010
+ * Process a single SSE event
2011
+ * Returns exit code if we should exit, or -1 to continue
2012
+ */
2013
+ function processSSEEvent(eventType, eventData, raw, showResult) {
2014
+ let parsed;
2015
+ try {
2016
+ parsed = JSON.parse(eventData);
2017
+ }
2018
+ catch {
2019
+ // If we can't parse, treat as raw text
2020
+ if (raw) {
2021
+ console.log(eventData);
2022
+ }
2023
+ return -1;
2024
+ }
2025
+ // Handle different event types
2026
+ switch (eventType) {
2027
+ case 'log': {
2028
+ const logEvent = parsed;
2029
+ if (raw) {
2030
+ console.log(JSON.stringify(logEvent));
2031
+ }
2032
+ else {
2033
+ formatFollowLogLine(logEvent);
2034
+ }
2035
+ return -1;
2036
+ }
2037
+ case 'complete': {
2038
+ const completeEvent = parsed;
2039
+ if (raw) {
2040
+ console.log(JSON.stringify(completeEvent));
2041
+ }
2042
+ else {
2043
+ console.log('[+] Completed');
2044
+ if (showResult && completeEvent.result) {
2045
+ console.log('');
2046
+ console.log(completeEvent.result);
2047
+ }
2048
+ }
2049
+ return completeEvent.exit_code ?? 0;
2050
+ }
2051
+ case 'error': {
2052
+ const errorEvent = parsed;
2053
+ if (raw) {
2054
+ console.log(JSON.stringify(errorEvent));
2055
+ }
2056
+ else {
2057
+ console.log('[!] Error');
2058
+ if (errorEvent.error) {
2059
+ console.log(` ${errorEvent.error}`);
2060
+ }
2061
+ }
2062
+ return errorEvent.exit_code ?? 1;
2063
+ }
2064
+ default: {
2065
+ // Unknown event type, still output if raw mode
2066
+ if (raw) {
2067
+ console.log(JSON.stringify(parsed));
2068
+ }
2069
+ return -1;
2070
+ }
2071
+ }
2072
+ }
2073
+ /**
2074
+ * Format a log line for human-readable output
2075
+ */
2076
+ function formatFollowLogLine(event) {
2077
+ const timestamp = new Date(event.timestamp).toLocaleTimeString();
2078
+ const line = event.line;
2079
+ const type = event.type || line.type || 'log';
2080
+ // If type starts with 'lifecycle_', format as lifecycle event
2081
+ if (type.startsWith('lifecycle_')) {
2082
+ const content = line;
2083
+ const phase = content.phase || 'unknown';
2084
+ const action = content.action || 'unknown';
2085
+ const duration = content.duration_ms;
2086
+ const success = content.success;
2087
+ const error = content.error;
2088
+ const meta = content.meta || {};
2089
+ if (action === 'start') {
2090
+ const detail = formatLifecycleMeta(phase, meta);
2091
+ console.log(`[${timestamp}] ${getLifecycleIcon(phase)} Starting ${phase}${detail}...`);
2092
+ }
2093
+ else if (action === 'end') {
2094
+ const durationStr = duration ? ` (${duration}ms)` : '';
2095
+ if (success === false && error) {
2096
+ console.log(`[${timestamp}] ${getLifecycleIcon(phase)} ${capitalize(phase)} failed${durationStr}: ${error}`);
2097
+ }
2098
+ else {
2099
+ console.log(`[${timestamp}] ${getLifecycleIcon(phase)} ${capitalize(phase)} completed${durationStr}`);
2100
+ }
2101
+ }
2102
+ else if (action === 'log') {
2103
+ const msg = meta.message || JSON.stringify(meta);
2104
+ console.log(`[${timestamp}] > ${msg}`);
2105
+ }
2106
+ return;
2107
+ }
2108
+ // Extract common fields
2109
+ const message = line.message || line.text || '';
2110
+ const tool = line.tool;
2111
+ const toolInput = line.tool_input;
2112
+ // Format based on event/log type
2113
+ switch (type) {
2114
+ case 'assistant':
2115
+ case 'text':
2116
+ if (message) {
2117
+ console.log(`[${timestamp}] ${message}`);
2118
+ }
2119
+ break;
2120
+ case 'tool_use':
2121
+ if (tool) {
2122
+ // Show tool name with optional truncated input
2123
+ const inputPreview = toolInput ? ` ${toolInput.substring(0, 60)}${toolInput.length > 60 ? '...' : ''}` : '';
2124
+ console.log(`[${timestamp}] Tool: ${tool}${inputPreview}`);
2125
+ }
2126
+ break;
2127
+ case 'tool_result':
2128
+ // Don't show tool results in follow mode (too verbose)
2129
+ break;
2130
+ case 'status':
2131
+ console.log(`[${timestamp}] ${message || JSON.stringify(line)}`);
2132
+ break;
2133
+ case 'error':
2134
+ console.log(`[${timestamp}] Error: ${message || JSON.stringify(line)}`);
2135
+ break;
2136
+ default:
2137
+ // For unknown types, show as JSON if it has content
2138
+ if (message) {
2139
+ console.log(`[${timestamp}] ${message}`);
2140
+ }
2141
+ else if (Object.keys(line).length > 0) {
2142
+ console.log(`[${timestamp}] ${JSON.stringify(line)}`);
2143
+ }
2144
+ }
2145
+ }
2146
+ /**
2147
+ * Get status icon for attempt status
2148
+ */
2149
+ function getStatusIcon(status) {
2150
+ switch (status) {
2151
+ case 'pending':
2152
+ return '[⏸]';
2153
+ case 'running':
2154
+ return '[▶]';
2155
+ case 'succeeded':
2156
+ return '[✓]';
2157
+ case 'failed':
2158
+ return '[✗]';
2159
+ case 'cancelled':
2160
+ return '[○]';
2161
+ default:
2162
+ return '[ ]';
2163
+ }
2164
+ }
2165
+ /**
2166
+ * Format duration between two dates
2167
+ */
2168
+ function formatDuration(startStr, endStr) {
2169
+ try {
2170
+ const start = new Date(startStr).getTime();
2171
+ const end = new Date(endStr).getTime();
2172
+ const durationMs = end - start;
2173
+ const seconds = Math.floor(durationMs / 1000);
2174
+ const minutes = Math.floor(seconds / 60);
2175
+ const hours = Math.floor(minutes / 60);
2176
+ if (hours > 0) {
2177
+ const remainingMinutes = minutes % 60;
2178
+ return `${hours}h ${remainingMinutes}m`;
2179
+ }
2180
+ if (minutes > 0) {
2181
+ const remainingSeconds = seconds % 60;
2182
+ return `${minutes}m ${remainingSeconds}s`;
2183
+ }
2184
+ return `${seconds}s`;
2185
+ }
2186
+ catch {
2187
+ return 'unknown';
2188
+ }
2189
+ }
2190
+ /**
2191
+ * Pad a string to the right with spaces
2192
+ */
2193
+ function padRight(str, width) {
2194
+ if (str.length >= width)
2195
+ return str;
2196
+ return str + ' '.repeat(width - str.length);
2197
+ }
2198
+ /**
2199
+ * eve job runner-logs <id> [--attempt N]
2200
+ * Stream K8s runner pod logs for a job
2201
+ */
2202
+ async function handleRunnerLogs(positionals, flags, context) {
2203
+ const jobId = positionals[0];
2204
+ if (!jobId) {
2205
+ throw new Error('Usage: eve job runner-logs <job-id> [--attempt N]');
2206
+ }
2207
+ const attemptStr = (0, args_1.getStringFlag)(flags, ['attempt']);
2208
+ // Fetch job with attempts to get runtime_meta
2209
+ const attemptsResponse = await (0, client_1.requestJson)(context, `/jobs/${jobId}/attempts`);
2210
+ if (!attemptsResponse.attempts || attemptsResponse.attempts.length === 0) {
2211
+ console.error(`No attempts found for job ${jobId}`);
2212
+ process.exit(1);
2213
+ }
2214
+ // Select target attempt
2215
+ let targetAttempt;
2216
+ if (attemptStr) {
2217
+ const attemptNum = parseInt(attemptStr, 10);
2218
+ targetAttempt = attemptsResponse.attempts.find(a => a.attempt_number === attemptNum);
2219
+ if (!targetAttempt) {
2220
+ console.error(`Attempt #${attemptNum} not found for job ${jobId}`);
2221
+ process.exit(1);
2222
+ }
2223
+ }
2224
+ else {
2225
+ // Use latest attempt
2226
+ targetAttempt = attemptsResponse.attempts[attemptsResponse.attempts.length - 1];
2227
+ }
2228
+ if (!targetAttempt) {
2229
+ console.error(`No attempt found for job ${jobId}`);
2230
+ process.exit(1);
2231
+ }
2232
+ console.log(`Streaming logs for job ${jobId}, attempt #${targetAttempt.attempt_number}...`);
2233
+ // Try to get pod_name from runtime_meta
2234
+ const podName = targetAttempt.runtime_meta?.pod_name;
2235
+ let kubectlArgs;
2236
+ if (podName) {
2237
+ // Use pod name directly
2238
+ console.log(`Using pod: ${podName}`);
2239
+ kubectlArgs = ['-n', 'eve', 'logs', '-f', podName];
2240
+ }
2241
+ else {
2242
+ // Fall back to label-based lookup
2243
+ console.log(`No pod_name in runtime_meta, using label selector: job-id=${jobId}`);
2244
+ kubectlArgs = ['-n', 'eve', 'logs', '-f', '-l', `job-id=${jobId}`];
2245
+ }
2246
+ // Spawn kubectl process
2247
+ const kubectl = (0, child_process_1.spawn)('kubectl', kubectlArgs, {
2248
+ stdio: 'inherit',
2249
+ });
2250
+ // Handle process exit
2251
+ return new Promise((resolve, reject) => {
2252
+ kubectl.on('error', (error) => {
2253
+ reject(new Error(`Failed to execute kubectl: ${error.message}`));
2254
+ });
2255
+ kubectl.on('exit', (code) => {
2256
+ if (code === 0) {
2257
+ resolve();
2258
+ }
2259
+ else {
2260
+ // Non-zero exit is common (e.g., pod terminated, not found)
2261
+ // Don't treat as error
2262
+ resolve();
2263
+ }
2264
+ });
2265
+ });
2266
+ }