@shipfox/api-agent-access 21.0.0 → 21.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +38 -0
  3. package/dist/core/log-tools.d.ts +10 -0
  4. package/dist/core/log-tools.d.ts.map +1 -0
  5. package/dist/core/log-tools.js +165 -0
  6. package/dist/core/log-tools.js.map +1 -0
  7. package/dist/core/paged-tools.d.ts.map +1 -1
  8. package/dist/core/paged-tools.js +8 -50
  9. package/dist/core/paged-tools.js.map +1 -1
  10. package/dist/core/response.d.ts.map +1 -1
  11. package/dist/core/response.js +57 -18
  12. package/dist/core/response.js.map +1 -1
  13. package/dist/core/tool-utils.d.ts +37 -0
  14. package/dist/core/tool-utils.d.ts.map +1 -0
  15. package/dist/core/tool-utils.js +73 -0
  16. package/dist/core/tool-utils.js.map +1 -0
  17. package/dist/core/workflow-diagnostic-tools.d.ts +5 -0
  18. package/dist/core/workflow-diagnostic-tools.d.ts.map +1 -0
  19. package/dist/core/workflow-diagnostic-tools.js +631 -0
  20. package/dist/core/workflow-diagnostic-tools.js.map +1 -0
  21. package/dist/core/workflow-tools.d.ts +4 -0
  22. package/dist/core/workflow-tools.d.ts.map +1 -0
  23. package/dist/core/workflow-tools.js +434 -0
  24. package/dist/core/workflow-tools.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/tsconfig.test.tsbuildinfo +1 -1
  30. package/package.json +7 -6
  31. package/src/core/diagnostic-tools.test.ts +33 -0
  32. package/src/core/log-tools.test.ts +370 -0
  33. package/src/core/log-tools.ts +271 -0
  34. package/src/core/paged-tools.test.ts +19 -2
  35. package/src/core/paged-tools.ts +18 -67
  36. package/src/core/response.ts +67 -19
  37. package/src/core/tool-utils.ts +115 -0
  38. package/src/core/workflow-diagnostic-tools.test.ts +693 -0
  39. package/src/core/workflow-diagnostic-tools.ts +840 -0
  40. package/src/core/workflow-execution-event-tools.test.ts +324 -0
  41. package/src/core/workflow-tools.test.ts +531 -0
  42. package/src/core/workflow-tools.ts +514 -0
  43. package/src/index.ts +5 -0
  44. package/src/presentation/mcp-server.test.ts +57 -1
  45. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,531 @@
1
+ import type {
2
+ GetWorkflowJobResultDto,
3
+ GetWorkflowRunResultDto,
4
+ ListWorkflowExecutionStepsResultDto,
5
+ ListWorkflowJobExecutionsResultDto,
6
+ ListWorkflowRunAttemptsResultDto,
7
+ ListWorkflowRunJobsResultDto,
8
+ ListWorkflowStepAttemptsResultDto,
9
+ } from '@shipfox/api-agent-access-dto';
10
+ import {
11
+ type AgentAccessEnvelopeDto,
12
+ agentAccessEnvelopeSchema,
13
+ } from '@shipfox/api-agent-access-dto';
14
+ import type {AgentAccessContext} from '@shipfox/api-auth-context';
15
+ import type {
16
+ JobExecutionSummaryDto,
17
+ StepAttemptSummaryDto,
18
+ StepSummaryDto,
19
+ WorkflowJobDetailDto,
20
+ WorkflowRunJobOverviewDto,
21
+ WorkflowRunOverviewResponseDto,
22
+ } from '@shipfox/api-workflows-dto';
23
+ import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
24
+ import {
25
+ decodeStringIdCursor,
26
+ encodeNumberIdCursor,
27
+ encodeStringIdCursor,
28
+ } from '@shipfox/node-drizzle';
29
+ import {createAgentAccessTools} from './paged-tools.js';
30
+ import {serializedAgentAccessEnvelopeByteLength} from './response.js';
31
+
32
+ const workspaceId = uuid(1);
33
+ const runId = uuid(2);
34
+ const projectId = uuid(3);
35
+ const definitionId = uuid(4);
36
+ const jobId = uuid(5);
37
+ const executionId = uuid(6);
38
+ const stepId = uuid(7);
39
+ const attemptId = uuid(8);
40
+ const isoDate = '2026-08-01T00:00:00.000Z';
41
+ const context: AgentAccessContext = {
42
+ userId: uuid(9),
43
+ workspaceId,
44
+ scopes: ['read'],
45
+ credential: {kind: 'oauth_grant', grantId: uuid(10), clientId: 'client-1'},
46
+ };
47
+
48
+ describe('bounded workflow agent-access tools', () => {
49
+ test('registers the complete progressive traversal without a workspace argument', () => {
50
+ const mocks = clients();
51
+ const tools = createAgentAccessTools(mocks).filter((tool) => tool.name.includes('workflow'));
52
+
53
+ expect(tools.map((tool) => tool.name)).toEqual([
54
+ 'list_workflow_definitions',
55
+ 'list_workflow_runs',
56
+ 'get_workflow_run',
57
+ 'list_workflow_run_attempts',
58
+ 'list_workflow_run_jobs',
59
+ 'get_workflow_job',
60
+ 'list_workflow_job_executions',
61
+ 'list_workflow_execution_steps',
62
+ 'list_workflow_step_attempts',
63
+ ]);
64
+ for (const tool of tools) {
65
+ expect(tool.annotations).toEqual({readOnlyHint: true});
66
+ expect(tool.inputSchema.properties).not.toHaveProperty('workspace_id');
67
+ }
68
+ });
69
+
70
+ test('returns a compact selected-attempt summary and pins the latest attempt', async () => {
71
+ const mocks = clients();
72
+ mocks.workflows.getWorkflowRunOverview.mockResolvedValue(overview());
73
+
74
+ const response = await tool(mocks, 'get_workflow_run').execute({
75
+ context,
76
+ arguments: {run_id: runId},
77
+ });
78
+ const result = expectSuccess<GetWorkflowRunResultDto>(response);
79
+
80
+ expect(mocks.workflows.getWorkflowRunOverview).toHaveBeenCalledWith({
81
+ workspaceId,
82
+ workflowRunId: runId,
83
+ attempt: undefined,
84
+ });
85
+ expect(result).toMatchObject({
86
+ id: runId,
87
+ status: 'failed',
88
+ attempt: {attempt: 2, workflow_run_id: runId},
89
+ job_status_counts: [
90
+ {status: 'failed', count: 1},
91
+ {status: 'succeeded', count: 1},
92
+ ],
93
+ has_started_job_execution: true,
94
+ });
95
+ expect(result).not.toHaveProperty('jobs');
96
+ expect(result).not.toHaveProperty('trigger_payload');
97
+ });
98
+
99
+ test('passes an explicit run attempt through to the bounded overview', async () => {
100
+ const mocks = clients();
101
+ mocks.workflows.getWorkflowRunOverview.mockResolvedValue(overview(1));
102
+
103
+ await tool(mocks, 'get_workflow_run').execute({
104
+ context,
105
+ arguments: {run_id: runId, attempt: 1},
106
+ });
107
+
108
+ expect(mocks.workflows.getWorkflowRunOverview).toHaveBeenCalledWith({
109
+ workspaceId,
110
+ workflowRunId: runId,
111
+ attempt: 1,
112
+ });
113
+ });
114
+
115
+ test('relays status counts from the large workflow overview variant', async () => {
116
+ const mocks = clients();
117
+ mocks.workflows.getWorkflowRunOverview.mockResolvedValue({
118
+ ...overview(),
119
+ jobs: {
120
+ kind: 'large',
121
+ total: 250,
122
+ status_counts: [
123
+ {status: 'failed', count: 3},
124
+ {status: 'succeeded', count: 247},
125
+ ],
126
+ first_page: {items: [], next_cursor: null, total: 100},
127
+ },
128
+ });
129
+
130
+ const response = await tool(mocks, 'get_workflow_run').execute({
131
+ context,
132
+ arguments: {run_id: runId},
133
+ });
134
+ const result = expectSuccess<GetWorkflowRunResultDto>(response);
135
+
136
+ expect(result.job_status_counts).toEqual([
137
+ {status: 'failed', count: 3},
138
+ {status: 'succeeded', count: 247},
139
+ ]);
140
+ });
141
+
142
+ test('pages run attempts with the producer cursor and exposes run coordinates', async () => {
143
+ const mocks = clients();
144
+ const nextCursor = encodeNumberIdCursor({value: 1, id: attemptId});
145
+ mocks.workflows.listWorkflowRunAttempts.mockResolvedValue({
146
+ items: [runAttempt(2), runAttempt(1)],
147
+ nextCursor,
148
+ });
149
+
150
+ const response = await tool(mocks, 'list_workflow_run_attempts').execute({
151
+ context,
152
+ arguments: {run_id: runId, limit: 2},
153
+ });
154
+ const result = expectSuccess<ListWorkflowRunAttemptsResultDto>(response);
155
+
156
+ expect(mocks.workflows.listWorkflowRunAttempts).toHaveBeenCalledWith({
157
+ workspaceId,
158
+ workflowRunId: runId,
159
+ limit: 2,
160
+ cursor: undefined,
161
+ });
162
+ expect(result.attempts).toHaveLength(2);
163
+ expect(result.attempts[0]).toMatchObject({workflow_run_id: runId, attempt: 2});
164
+ expect(result.next_cursor).toBe(nextCursor);
165
+ expect(agentAccessEnvelopeSchema.safeParse(response).success).toBe(true);
166
+ });
167
+
168
+ test('pages jobs for a pinned attempt and excludes dependency data', async () => {
169
+ const mocks = clients();
170
+ const producerCursor = encodeStringIdCursor({value: '0', id: jobId});
171
+ mocks.workflows.listWorkflowRunJobs.mockResolvedValue({
172
+ workflow_run_attempt: 2,
173
+ items: [{...job(), dependencies: ['other-job']}],
174
+ nextCursor: producerCursor,
175
+ total: 1,
176
+ });
177
+
178
+ const response = await tool(mocks, 'list_workflow_run_jobs').execute({
179
+ context,
180
+ arguments: {run_id: runId, attempt: 2, limit: 1},
181
+ });
182
+ const result = expectSuccess<ListWorkflowRunJobsResultDto>(response);
183
+
184
+ expect(mocks.workflows.listWorkflowRunJobs).toHaveBeenCalledWith({
185
+ workspaceId,
186
+ workflowRunId: runId,
187
+ attempt: 2,
188
+ limit: 1,
189
+ cursor: undefined,
190
+ });
191
+ expect(result).toMatchObject({
192
+ workflow_run_id: runId,
193
+ workflow_run_attempt: 2,
194
+ jobs: [{id: jobId, position: 0}],
195
+ total: 1,
196
+ });
197
+ expect(result.jobs[0]).not.toHaveProperty('dependencies');
198
+ });
199
+
200
+ test('shortens an oversized job page and anchors its cursor to the retained job', async () => {
201
+ const mocks = clients();
202
+ const jobs = Array.from({length: 100}, (_, index) => ({
203
+ ...job(),
204
+ id: uuid(100 + index),
205
+ key: 'k'.repeat(20_000),
206
+ name: 'n'.repeat(20_000),
207
+ position: index,
208
+ default_execution: execution(uuid(200 + index), index + 1, 'e'.repeat(20_000)),
209
+ }));
210
+ mocks.workflows.listWorkflowRunJobs.mockResolvedValue({
211
+ workflow_run_attempt: 2,
212
+ items: jobs,
213
+ nextCursor: null,
214
+ total: 100,
215
+ });
216
+
217
+ const response = await tool(mocks, 'list_workflow_run_jobs').execute({
218
+ context,
219
+ arguments: {run_id: runId, attempt: 2, limit: 100},
220
+ });
221
+ const result = expectSuccess<ListWorkflowRunJobsResultDto>(response);
222
+ const last = result.jobs.at(-1);
223
+
224
+ expect(result.jobs.length).toBeLessThan(100);
225
+ expect(response).toMatchObject({ok: true, response_truncated: true});
226
+ expect(response.response_total_bytes).toBeGreaterThan(128 * 1024);
227
+ expect(serializedAgentAccessEnvelopeByteLength(response)).toBeLessThanOrEqual(128 * 1024);
228
+ expect(last).toBeDefined();
229
+ expect(decodeStringIdCursor(result.next_cursor ?? undefined)).toEqual({
230
+ value: String(last?.position),
231
+ id: last?.id,
232
+ });
233
+ });
234
+
235
+ test('returns a compact job while leaving selected child collections lazy', async () => {
236
+ const mocks = clients();
237
+ mocks.workflows.getWorkflowJobDetail.mockResolvedValue(jobDetail());
238
+
239
+ const response = await tool(mocks, 'get_workflow_job').execute({
240
+ context,
241
+ arguments: {job_id: jobId, execution_id: executionId},
242
+ });
243
+ const result = expectSuccess<GetWorkflowJobResultDto>(response);
244
+
245
+ expect(mocks.workflows.getWorkflowJobDetail).toHaveBeenCalledWith({
246
+ workspaceId,
247
+ jobId,
248
+ executionId,
249
+ });
250
+ expect(result).toMatchObject({
251
+ workflow_run_id: runId,
252
+ workflow_run_attempt: 2,
253
+ job: {id: jobId},
254
+ selected_execution: {id: executionId, has_context: true},
255
+ });
256
+ expect(result.job).not.toHaveProperty('dependencies');
257
+ expect(result.selected_execution).not.toHaveProperty('steps');
258
+ expect(JSON.stringify(result)).not.toContain('source_location');
259
+ expect(JSON.stringify(result)).not.toContain('gate_result');
260
+ expect(JSON.stringify(result)).not.toContain('error');
261
+ });
262
+
263
+ test('supports jobs without an execution without fabricating a child id', async () => {
264
+ const mocks = clients();
265
+ mocks.workflows.getWorkflowJobDetail.mockResolvedValue({
266
+ ...jobDetail(),
267
+ selected_execution: null,
268
+ });
269
+
270
+ const response = await tool(mocks, 'get_workflow_job').execute({
271
+ context,
272
+ arguments: {job_id: jobId},
273
+ });
274
+ const result = expectSuccess<GetWorkflowJobResultDto>(response);
275
+
276
+ expect(result.selected_execution).toBeNull();
277
+ });
278
+
279
+ test('pages execution history with job coordinates and caps large text fields', async () => {
280
+ const mocks = clients();
281
+ const executions = Array.from({length: 100}, (_, index) =>
282
+ execution(uuid(100 + index), index + 1, 'x'.repeat(20_000)),
283
+ );
284
+ mocks.workflows.listWorkflowJobExecutions.mockResolvedValue({
285
+ items: executions,
286
+ nextCursor: null,
287
+ total: 100,
288
+ });
289
+
290
+ const response = await tool(mocks, 'list_workflow_job_executions').execute({
291
+ context,
292
+ arguments: {job_id: jobId, limit: 100},
293
+ });
294
+ const result = expectSuccess<ListWorkflowJobExecutionsResultDto>(response);
295
+
296
+ expect(result.job_id).toBe(jobId);
297
+ expect(result.executions).toHaveLength(100);
298
+ expect(result.executions[0]?.name).toHaveLength(512);
299
+ expect(response).toMatchObject({ok: true});
300
+ expect(result.next_cursor).toBeNull();
301
+ });
302
+
303
+ test('pages steps and attempts with the canonical parent coordinates', async () => {
304
+ const mocks = clients();
305
+ mocks.workflows.listWorkflowExecutionSteps.mockResolvedValue({
306
+ items: [step()],
307
+ nextCursor: null,
308
+ total: 1,
309
+ });
310
+ mocks.workflows.listWorkflowStepAttempts.mockResolvedValue({
311
+ items: [stepAttempt()],
312
+ nextCursor: null,
313
+ total: 1,
314
+ stepType: 'run',
315
+ });
316
+
317
+ const stepsResponse = await tool(mocks, 'list_workflow_execution_steps').execute({
318
+ context,
319
+ arguments: {job_id: jobId, execution_id: executionId},
320
+ });
321
+ const steps = expectSuccess<ListWorkflowExecutionStepsResultDto>(stepsResponse);
322
+ expect(steps).toMatchObject({
323
+ job_id: jobId,
324
+ execution_id: executionId,
325
+ steps: [{id: stepId}],
326
+ });
327
+ expect(steps.steps[0]).not.toHaveProperty('attempts');
328
+ expect(steps.steps[0]).not.toHaveProperty('source_location');
329
+
330
+ const attemptsResponse = await tool(mocks, 'list_workflow_step_attempts').execute({
331
+ context,
332
+ arguments: {step_id: stepId},
333
+ });
334
+ const attempts = expectSuccess<ListWorkflowStepAttemptsResultDto>(attemptsResponse);
335
+ expect(attempts).toMatchObject({step_id: stepId, attempts: [{id: attemptId, attempt: 1}]});
336
+ expect(attempts.attempts[0]).not.toHaveProperty('error');
337
+ expect(attempts.attempts[0]).not.toHaveProperty('gate_result');
338
+ });
339
+
340
+ test('turns inaccessible or malformed traversal reads into bounded tool errors', async () => {
341
+ const mocks = clients();
342
+ mocks.workflows.listWorkflowRunJobs.mockResolvedValue(null);
343
+
344
+ const missing = await tool(mocks, 'list_workflow_run_jobs').execute({
345
+ context,
346
+ arguments: {run_id: runId, attempt: 2},
347
+ });
348
+ expect(missing).toEqual({ok: false, error: {code: 'not-found'}});
349
+
350
+ const invalid = await tool(mocks, 'list_workflow_run_jobs').execute({
351
+ context,
352
+ arguments: {run_id: runId, attempt: 2, cursor: 'not-a-cursor'},
353
+ });
354
+ expect(invalid).toEqual({ok: false, error: {code: 'invalid-request'}});
355
+ expect(mocks.workflows.listWorkflowRunJobs).toHaveBeenCalledTimes(1);
356
+ });
357
+ });
358
+
359
+ type WorkflowMocks = WorkflowsModuleClient & {
360
+ listWorkflowRuns: ReturnType<typeof vi.fn>;
361
+ getLatestRunAttempt: ReturnType<typeof vi.fn>;
362
+ getWorkflowRunOverview: ReturnType<typeof vi.fn>;
363
+ listWorkflowRunAttempts: ReturnType<typeof vi.fn>;
364
+ listWorkflowRunJobs: ReturnType<typeof vi.fn>;
365
+ getWorkflowJobDetail: ReturnType<typeof vi.fn>;
366
+ listWorkflowJobExecutions: ReturnType<typeof vi.fn>;
367
+ listWorkflowExecutionSteps: ReturnType<typeof vi.fn>;
368
+ listWorkflowStepAttempts: ReturnType<typeof vi.fn>;
369
+ };
370
+
371
+ type AgentAccessTestClients = Parameters<typeof createAgentAccessTools>[0] & {
372
+ workflows: WorkflowMocks;
373
+ };
374
+
375
+ function clients(): AgentAccessTestClients {
376
+ return {
377
+ projects: {
378
+ listProjectCatalogByWorkspace: vi.fn(),
379
+ requireProjectForWorkspace: vi.fn(),
380
+ },
381
+ definitions: {listDefinitionsByProject: vi.fn()},
382
+ workflows: {
383
+ listWorkflowRuns: vi.fn(),
384
+ getLatestRunAttempt: vi.fn(),
385
+ getWorkflowRunOverview: vi.fn(),
386
+ listWorkflowRunAttempts: vi.fn(),
387
+ listWorkflowRunJobs: vi.fn(),
388
+ getWorkflowJobDetail: vi.fn(),
389
+ listWorkflowJobExecutions: vi.fn(),
390
+ listWorkflowExecutionSteps: vi.fn(),
391
+ listWorkflowStepAttempts: vi.fn(),
392
+ } as unknown as WorkflowMocks,
393
+ annotations: {listAnnotationsForRunAttempt: vi.fn()},
394
+ triggers: {listTriggerEvents: vi.fn()},
395
+ } as unknown as AgentAccessTestClients;
396
+ }
397
+
398
+ function tool(mocks: ReturnType<typeof clients>, name: string) {
399
+ const candidate = createAgentAccessTools(mocks).find((entry) => entry.name === name);
400
+ if (!candidate) throw new Error(`Missing tool ${name}`);
401
+ return candidate;
402
+ }
403
+
404
+ function expectSuccess<T>(response: AgentAccessEnvelopeDto): T {
405
+ expect(response.ok).toBe(true);
406
+ if (!response.ok) throw new Error('Expected a successful tool response');
407
+ expect(agentAccessEnvelopeSchema.safeParse(response).success).toBe(true);
408
+ return response.result as T;
409
+ }
410
+
411
+ function overview(attempt = 2): WorkflowRunOverviewResponseDto {
412
+ return {
413
+ run: {
414
+ id: runId,
415
+ project_id: projectId,
416
+ definition_id: definitionId,
417
+ number: 1,
418
+ name: 'Run',
419
+ workflow_name: 'Workflow',
420
+ origin: 'synced',
421
+ dev_source: null,
422
+ trigger_provider: 'github',
423
+ trigger_source: 'push',
424
+ trigger_event: 'push',
425
+ trigger_reference: {repository: 'shipfox/platform', ref: 'main', commit: 'abc', actor: 'noe'},
426
+ created_at: isoDate,
427
+ },
428
+ attempt: runAttempt(attempt),
429
+ has_started_job_execution: true,
430
+ jobs: {
431
+ kind: 'complete',
432
+ total: 2,
433
+ items: [job(), {...job(), id: uuid(11), status: 'succeeded', default_execution: null}],
434
+ },
435
+ };
436
+ }
437
+
438
+ function runAttempt(attempt: number) {
439
+ return {
440
+ id: attempt === 2 ? attemptId : uuid(12),
441
+ workflow_run_id: runId,
442
+ attempt,
443
+ status: attempt === 2 ? ('failed' as const) : ('succeeded' as const),
444
+ created_at: isoDate,
445
+ started_at: isoDate,
446
+ finished_at: isoDate,
447
+ rerun_mode: null,
448
+ };
449
+ }
450
+
451
+ function job(): WorkflowRunJobOverviewDto {
452
+ return {
453
+ id: jobId,
454
+ key: 'build',
455
+ name: 'Build',
456
+ position: 0,
457
+ dependencies: [],
458
+ status: 'failed',
459
+ status_reason: 'step_failed',
460
+ mode: 'one_shot',
461
+ listener_status: 'inactive',
462
+ carried_over: false,
463
+ execution_count: 1,
464
+ execution_status_counts: {pending: 0, running: 0, succeeded: 0, failed: 1, cancelled: 0},
465
+ default_execution: execution(executionId),
466
+ };
467
+ }
468
+
469
+ function execution(id: string, sequence = 1, name = 'Build execution'): JobExecutionSummaryDto {
470
+ return {
471
+ id,
472
+ sequence,
473
+ name,
474
+ status: 'failed',
475
+ display_status: 'failed',
476
+ status_reason: 'step_failed',
477
+ status_reason_message: 'The step failed',
478
+ queued_at: isoDate,
479
+ started_at: isoDate,
480
+ finished_at: isoDate,
481
+ timed_out_at: null,
482
+ updated_at: isoDate,
483
+ };
484
+ }
485
+
486
+ function jobDetail(): WorkflowJobDetailDto {
487
+ return {
488
+ workflow_run_id: runId,
489
+ workflow_run_attempt: 2,
490
+ job: job(),
491
+ selected_execution: {
492
+ ...execution(executionId),
493
+ has_context: true,
494
+ steps: {items: [step()], next_cursor: null, total: 1},
495
+ },
496
+ };
497
+ }
498
+
499
+ function step(): StepSummaryDto {
500
+ return {
501
+ id: stepId,
502
+ key: 'run',
503
+ name: 'Run command',
504
+ type: 'run',
505
+ position: 0,
506
+ status: 'failed',
507
+ status_reason: null,
508
+ source_location: {start_line: 1, end_line: 2},
509
+ current_attempt: 1,
510
+ error: {message: 'do not expose'},
511
+ attempts: {items: [stepAttempt()], next_cursor: null, total: 1},
512
+ };
513
+ }
514
+
515
+ function stepAttempt(): StepAttemptSummaryDto {
516
+ return {
517
+ id: attemptId,
518
+ attempt: 1,
519
+ execution_order: 1,
520
+ status: 'failed',
521
+ exit_code: 1,
522
+ started_at: isoDate,
523
+ finished_at: isoDate,
524
+ error: {message: 'do not expose'},
525
+ gate_result: {kind: 'none'},
526
+ };
527
+ }
528
+
529
+ function uuid(value: number): string {
530
+ return `00000000-0000-4000-8000-${String(value).padStart(12, '0')}`;
531
+ }