@shipfox/api-workflows-dto 2.0.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 (87) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +46 -0
  5. package/LICENSE +21 -0
  6. package/dist/events.d.ts +316 -0
  7. package/dist/events.d.ts.map +1 -0
  8. package/dist/events.js +144 -0
  9. package/dist/events.js.map +1 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +6 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/schemas/agent-runtime-config.d.ts +7 -0
  15. package/dist/schemas/agent-runtime-config.d.ts.map +1 -0
  16. package/dist/schemas/agent-runtime-config.js +7 -0
  17. package/dist/schemas/agent-runtime-config.js.map +1 -0
  18. package/dist/schemas/checkout-token.d.ts +56 -0
  19. package/dist/schemas/checkout-token.d.ts.map +1 -0
  20. package/dist/schemas/checkout-token.js +44 -0
  21. package/dist/schemas/checkout-token.js.map +1 -0
  22. package/dist/schemas/checkout.d.ts +10 -0
  23. package/dist/schemas/checkout.d.ts.map +1 -0
  24. package/dist/schemas/checkout.js +10 -0
  25. package/dist/schemas/checkout.js.map +1 -0
  26. package/dist/schemas/index.d.ts +11 -0
  27. package/dist/schemas/index.d.ts.map +1 -0
  28. package/dist/schemas/index.js +12 -0
  29. package/dist/schemas/index.js.map +1 -0
  30. package/dist/schemas/job-execution.d.ts +116 -0
  31. package/dist/schemas/job-execution.d.ts.map +1 -0
  32. package/dist/schemas/job-execution.js +42 -0
  33. package/dist/schemas/job-execution.js.map +1 -0
  34. package/dist/schemas/job-listening.d.ts +93 -0
  35. package/dist/schemas/job-listening.d.ts.map +1 -0
  36. package/dist/schemas/job-listening.js +60 -0
  37. package/dist/schemas/job-listening.js.map +1 -0
  38. package/dist/schemas/job.d.ts +101 -0
  39. package/dist/schemas/job.d.ts.map +1 -0
  40. package/dist/schemas/job.js +43 -0
  41. package/dist/schemas/job.js.map +1 -0
  42. package/dist/schemas/log-outcome.d.ts +7 -0
  43. package/dist/schemas/log-outcome.d.ts.map +1 -0
  44. package/dist/schemas/log-outcome.js +7 -0
  45. package/dist/schemas/log-outcome.js.map +1 -0
  46. package/dist/schemas/step.d.ts +186 -0
  47. package/dist/schemas/step.d.ts.map +1 -0
  48. package/dist/schemas/step.js +137 -0
  49. package/dist/schemas/step.js.map +1 -0
  50. package/dist/schemas/workflow-run-detail.d.ts +807 -0
  51. package/dist/schemas/workflow-run-detail.d.ts.map +1 -0
  52. package/dist/schemas/workflow-run-detail.js +51 -0
  53. package/dist/schemas/workflow-run-detail.js.map +1 -0
  54. package/dist/schemas/workflow-run.d.ts +213 -0
  55. package/dist/schemas/workflow-run.d.ts.map +1 -0
  56. package/dist/schemas/workflow-run.js +112 -0
  57. package/dist/schemas/workflow-run.js.map +1 -0
  58. package/dist/tsconfig.test.tsbuildinfo +1 -0
  59. package/package.json +55 -0
  60. package/src/events.test.ts +362 -0
  61. package/src/events.ts +180 -0
  62. package/src/index.ts +130 -0
  63. package/src/schemas/agent-runtime-config.test.ts +17 -0
  64. package/src/schemas/agent-runtime-config.ts +8 -0
  65. package/src/schemas/checkout-token.test.ts +162 -0
  66. package/src/schemas/checkout-token.ts +45 -0
  67. package/src/schemas/checkout.test.ts +49 -0
  68. package/src/schemas/checkout.ts +11 -0
  69. package/src/schemas/index.ts +104 -0
  70. package/src/schemas/job-execution.test.ts +27 -0
  71. package/src/schemas/job-execution.ts +92 -0
  72. package/src/schemas/job-listening.test.ts +64 -0
  73. package/src/schemas/job-listening.ts +59 -0
  74. package/src/schemas/job.test.ts +42 -0
  75. package/src/schemas/job.ts +53 -0
  76. package/src/schemas/log-outcome.ts +5 -0
  77. package/src/schemas/step-source-location.test.ts +45 -0
  78. package/src/schemas/step.test.ts +176 -0
  79. package/src/schemas/step.ts +163 -0
  80. package/src/schemas/workflow-run-detail.ts +67 -0
  81. package/src/schemas/workflow-run.test.ts +59 -0
  82. package/src/schemas/workflow-run.ts +144 -0
  83. package/tsconfig.build.json +9 -0
  84. package/tsconfig.build.tsbuildinfo +1 -0
  85. package/tsconfig.json +3 -0
  86. package/tsconfig.test.json +8 -0
  87. package/vitest.config.ts +3 -0
@@ -0,0 +1,64 @@
1
+ import {
2
+ jobListeningSchema,
3
+ triggerEventsBatchSchema,
4
+ workflowExecutionContextSchema,
5
+ } from './job-listening.js';
6
+
7
+ describe('jobListeningSchema', () => {
8
+ it('parses listener config payloads unchanged', () => {
9
+ const displayName = ['Review $', '{{ execution.index }}'].join('');
10
+ const input = {
11
+ on: [{source: 'github', event: 'pull_request_review'}],
12
+ until: [{source: 'github', event: 'pull_request'}],
13
+ timeout_ms: 1000,
14
+ max_executions: 3,
15
+ batch: {debounce_ms: 1000, max_size: 10, max_wait_ms: 5000},
16
+ on_resolve: 'finish',
17
+ execution_timeout_ms: null,
18
+ name: displayName,
19
+ };
20
+
21
+ const result = jobListeningSchema.parse(input);
22
+
23
+ expect(result).toEqual(input);
24
+ });
25
+ });
26
+
27
+ describe('execution context schemas', () => {
28
+ it('parses execution namespace payloads with event batches', () => {
29
+ const event = {
30
+ source: 'github',
31
+ event: 'pull_request_review',
32
+ delivery_id: 'delivery-1',
33
+ received_at: '2026-06-25T00:00:00.000Z',
34
+ data: {body: 'LGTM'},
35
+ };
36
+
37
+ const execution = workflowExecutionContextSchema.parse({
38
+ index: 0,
39
+ name: 'Review #1',
40
+ status: 'succeeded',
41
+ started_at: '2026-06-25T00:00:00.000Z',
42
+ finished_at: null,
43
+ events: [event],
44
+ });
45
+ const batch = triggerEventsBatchSchema.parse({events: [event]});
46
+
47
+ expect(execution.events).toEqual([event]);
48
+ expect(batch.events).toEqual([event]);
49
+ });
50
+
51
+ it('rejects negative execution indexes', () => {
52
+ const parse = () =>
53
+ workflowExecutionContextSchema.parse({
54
+ index: -1,
55
+ name: 'Review #-1',
56
+ status: 'succeeded',
57
+ started_at: '2026-06-25T00:00:00.000Z',
58
+ finished_at: null,
59
+ events: [],
60
+ });
61
+
62
+ expect(parse).toThrow();
63
+ });
64
+ });
@@ -0,0 +1,59 @@
1
+ import {z} from 'zod';
2
+
3
+ export const jobModeSchema = z.enum(['one_shot', 'listening']);
4
+ export const listenerStatusSchema = z.enum(['inactive', 'listening', 'resolved']);
5
+ export const resolutionReasonSchema = z.enum(['until', 'timeout', 'max_executions', 'cancelled']);
6
+
7
+ export const listeningTriggerSchema = z.object({
8
+ source: z.string(),
9
+ event: z.string(),
10
+ inputs: z.record(z.string(), z.unknown()).optional(),
11
+ filter: z.string().optional(),
12
+ });
13
+
14
+ export const jobListeningBatchSchema = z.object({
15
+ debounce_ms: z.number().int().positive().optional(),
16
+ max_size: z.number().int().positive().optional(),
17
+ max_wait_ms: z.number().int().positive().optional(),
18
+ });
19
+
20
+ export const jobListeningSchema = z.object({
21
+ on: z.array(listeningTriggerSchema).min(1),
22
+ until: z.array(listeningTriggerSchema).min(1).nullable(),
23
+ timeout_ms: z.number().int().positive().nullable(),
24
+ max_executions: z.number().int().positive().nullable(),
25
+ batch: jobListeningBatchSchema.nullable(),
26
+ on_resolve: z.enum(['finish', 'cancel']),
27
+ execution_timeout_ms: z.number().int().positive().nullable(),
28
+ name: z.string().nullable(),
29
+ });
30
+
31
+ export const workflowExecutionEventSchema = z.object({
32
+ source: z.string(),
33
+ event: z.string(),
34
+ delivery_id: z.string(),
35
+ received_at: z.string(),
36
+ data: z.unknown(),
37
+ });
38
+
39
+ export const workflowExecutionContextSchema = z.object({
40
+ index: z.number().int().nonnegative(),
41
+ name: z.string(),
42
+ status: z.string(),
43
+ started_at: z.string().nullable(),
44
+ finished_at: z.string().nullable(),
45
+ events: z.array(workflowExecutionEventSchema),
46
+ });
47
+
48
+ export const triggerEventsBatchSchema = z.object({
49
+ events: z.array(workflowExecutionEventSchema),
50
+ });
51
+
52
+ export type JobModeDto = z.infer<typeof jobModeSchema>;
53
+ export type ListenerStatusDto = z.infer<typeof listenerStatusSchema>;
54
+ export type ResolutionReasonDto = z.infer<typeof resolutionReasonSchema>;
55
+ export type ListeningTriggerDto = z.infer<typeof listeningTriggerSchema>;
56
+ export type JobListeningDto = z.infer<typeof jobListeningSchema>;
57
+ export type WorkflowExecutionEventDto = z.infer<typeof workflowExecutionEventSchema>;
58
+ export type WorkflowExecutionContextDto = z.infer<typeof workflowExecutionContextSchema>;
59
+ export type TriggerEventsBatchDto = z.infer<typeof triggerEventsBatchSchema>;
@@ -0,0 +1,42 @@
1
+ import {jobDtoSchema} from './job.js';
2
+
3
+ const baseJob = {
4
+ id: '11111111-1111-4111-8111-111111111111',
5
+ run_attempt_id: '22222222-2222-4222-8222-222222222222',
6
+ key: 'build',
7
+ name: 'build',
8
+ mode: 'one_shot',
9
+ status: 'pending',
10
+ status_reason: null,
11
+ carried_over: false,
12
+ listening: null,
13
+ listener_status: 'inactive',
14
+ resolution_reason: null,
15
+ outputs: null,
16
+ dependencies: [],
17
+ position: 0,
18
+ created_at: '2026-06-21T12:00:00.000Z',
19
+ updated_at: '2026-06-21T12:01:00.000Z',
20
+ };
21
+
22
+ describe('job DTO schema', () => {
23
+ it('accepts a job without execution timing fields', () => {
24
+ const result = jobDtoSchema.parse(baseJob);
25
+
26
+ expect(result).toMatchObject(baseJob);
27
+ });
28
+
29
+ it.each([
30
+ 'default_gate_rejected',
31
+ 'condition_rejected',
32
+ 'condition_errored',
33
+ ] as const)('accepts job skip reason "%s"', (statusReason) => {
34
+ const result = jobDtoSchema.parse({
35
+ ...baseJob,
36
+ status: 'skipped',
37
+ status_reason: statusReason,
38
+ });
39
+
40
+ expect(result.status_reason).toBe(statusReason);
41
+ });
42
+ });
@@ -0,0 +1,53 @@
1
+ import {z} from 'zod';
2
+ import {
3
+ jobListeningSchema,
4
+ jobModeSchema,
5
+ listenerStatusSchema,
6
+ resolutionReasonSchema,
7
+ } from './job-listening.js';
8
+
9
+ export const jobStatusSchema = z.enum([
10
+ 'pending',
11
+ 'running',
12
+ 'succeeded',
13
+ 'failed',
14
+ 'cancelled',
15
+ 'skipped',
16
+ ]);
17
+
18
+ export const jobStatusReasonSchema = z.enum([
19
+ 'dependency_not_completed',
20
+ 'condition_false',
21
+ 'default_gate_rejected',
22
+ 'condition_rejected',
23
+ 'condition_errored',
24
+ 'user_cancelled',
25
+ 'run_cancelled',
26
+ 'timed_out',
27
+ 'runner_lost',
28
+ 'step_failed',
29
+ 'unknown',
30
+ ]);
31
+
32
+ export const jobDtoSchema = z.object({
33
+ id: z.string().uuid(),
34
+ run_attempt_id: z.string().uuid(),
35
+ key: z.string(),
36
+ name: z.string().nullable(),
37
+ mode: jobModeSchema,
38
+ status: jobStatusSchema,
39
+ status_reason: jobStatusReasonSchema.nullable(),
40
+ carried_over: z.boolean(),
41
+ listening: jobListeningSchema.nullable(),
42
+ listener_status: listenerStatusSchema,
43
+ resolution_reason: resolutionReasonSchema.nullable(),
44
+ outputs: z.record(z.string(), z.unknown()).nullable(),
45
+ dependencies: z.array(z.string()),
46
+ position: z.number(),
47
+ created_at: z.string(),
48
+ updated_at: z.string(),
49
+ });
50
+
51
+ export type JobDto = z.infer<typeof jobDtoSchema>;
52
+ export type JobStatusDto = z.infer<typeof jobStatusSchema>;
53
+ export type JobStatusReasonDto = z.infer<typeof jobStatusReasonSchema>;
@@ -0,0 +1,5 @@
1
+ import {z} from 'zod';
2
+
3
+ export const logOutcomeSchema = z.enum(['drained', 'abandoned']);
4
+
5
+ export type LogOutcomeDto = z.infer<typeof logOutcomeSchema>;
@@ -0,0 +1,45 @@
1
+ import {stepDtoSchema, stepSourceLocationSchema} from './step.js';
2
+
3
+ const baseStep = {
4
+ id: '11111111-1111-4111-8111-111111111111',
5
+ job_execution_id: '33333333-3333-4333-8333-333333333333',
6
+ key: null,
7
+ name: 'echo hello',
8
+ status: 'pending',
9
+ type: 'run',
10
+ config: {run: 'echo hello'},
11
+ error: null,
12
+ position: 1,
13
+ current_attempt: 1,
14
+ created_at: '2026-06-16T00:00:00.000Z',
15
+ updated_at: '2026-06-16T00:00:00.000Z',
16
+ };
17
+
18
+ describe('step source location schemas', () => {
19
+ test('accepts valid source locations', () => {
20
+ const result = stepSourceLocationSchema.parse({start_line: 5, end_line: 8});
21
+
22
+ expect(result).toEqual({start_line: 5, end_line: 8});
23
+ });
24
+
25
+ test('rejects inverted source locations', () => {
26
+ const result = stepSourceLocationSchema.safeParse({start_line: 8, end_line: 5});
27
+
28
+ expect(result.success).toBe(false);
29
+ });
30
+
31
+ test('accepts step DTOs with source locations', () => {
32
+ const result = stepDtoSchema.parse({
33
+ ...baseStep,
34
+ source_location: {start_line: 5, end_line: 8},
35
+ });
36
+
37
+ expect(result.source_location).toEqual({start_line: 5, end_line: 8});
38
+ });
39
+
40
+ test('accepts step DTOs with null source locations', () => {
41
+ const result = stepDtoSchema.parse({...baseStep, source_location: null});
42
+
43
+ expect(result.source_location).toBeNull();
44
+ });
45
+ });
@@ -0,0 +1,176 @@
1
+ import {STEP_ERROR_MESSAGE_MAX_LENGTH, stepAttemptDtoSchema, stepErrorDtoSchema} from './step.js';
2
+
3
+ const baseAttempt = {
4
+ id: '11111111-1111-4111-8111-111111111111',
5
+ step_id: '22222222-2222-4222-8222-222222222222',
6
+ attempt: 1,
7
+ execution_order: 1,
8
+ status: 'succeeded',
9
+ exit_code: 0,
10
+ output: null,
11
+ outputs: null,
12
+ response: null,
13
+ error: null,
14
+ restart_feedback: null,
15
+ started_at: '2026-01-01T00:00:00.000Z',
16
+ finished_at: '2026-01-01T00:01:00.000Z',
17
+ };
18
+
19
+ describe('stepErrorDtoSchema', () => {
20
+ it('accepts a message at the maximum length', () => {
21
+ const result = stepErrorDtoSchema.safeParse({
22
+ message: 'x'.repeat(STEP_ERROR_MESSAGE_MAX_LENGTH),
23
+ });
24
+
25
+ expect(result.success).toBe(true);
26
+ });
27
+
28
+ it('rejects a message beyond the maximum length', () => {
29
+ const result = stepErrorDtoSchema.safeParse({
30
+ message: 'x'.repeat(STEP_ERROR_MESSAGE_MAX_LENGTH + 1),
31
+ });
32
+
33
+ expect(result.success).toBe(false);
34
+ });
35
+
36
+ it('accepts an agent config issue with an agent config failure reason', () => {
37
+ const result = stepErrorDtoSchema.parse({
38
+ message: 'Model provider credentials are not configured',
39
+ reason: 'agent_config_invalid',
40
+ agent_config_issue: 'provider_not_configured',
41
+ });
42
+
43
+ expect(result).toEqual({
44
+ message: 'Model provider credentials are not configured',
45
+ reason: 'agent_config_invalid',
46
+ agent_config_issue: 'provider_not_configured',
47
+ });
48
+ });
49
+
50
+ it('accepts typed output validation failures', () => {
51
+ const result = stepErrorDtoSchema.parse({
52
+ message: 'Output "count" must be a finite number or numeric string.',
53
+ reason: 'output_invalid',
54
+ field: 'outputs.count',
55
+ });
56
+
57
+ expect(result).toEqual({
58
+ message: 'Output "count" must be a finite number or numeric string.',
59
+ reason: 'output_invalid',
60
+ field: 'outputs.count',
61
+ });
62
+ });
63
+
64
+ it('rejects unknown agent config issues', () => {
65
+ const result = stepErrorDtoSchema.safeParse({
66
+ message: 'Model provider credentials are not configured',
67
+ reason: 'agent_config_invalid',
68
+ agent_config_issue: 'unknown',
69
+ });
70
+
71
+ expect(result.success).toBe(false);
72
+ });
73
+
74
+ it.each([
75
+ undefined,
76
+ 'workspace_prep_failed',
77
+ 'agent_invocation_failed',
78
+ ] as const)('rejects an agent config issue when reason is %s', (reason) => {
79
+ const result = stepErrorDtoSchema.safeParse({
80
+ message: 'Model provider credentials are not configured',
81
+ ...(reason === undefined ? {} : {reason}),
82
+ agent_config_issue: 'provider_not_configured',
83
+ });
84
+
85
+ expect(result.success).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('stepAttemptDtoSchema', () => {
90
+ it('accepts an attempt with no gate or restart feedback', () => {
91
+ const attempt = {...baseAttempt, gate_result: {kind: 'none'}};
92
+
93
+ const result = stepAttemptDtoSchema.parse(attempt);
94
+
95
+ expect(result.gate_result).toEqual({kind: 'none'});
96
+ expect(result.restart_feedback).toBeNull();
97
+ });
98
+
99
+ it('accepts not-evaluated and evaluation-error gate results', () => {
100
+ const notEvaluated = stepAttemptDtoSchema.parse({
101
+ ...baseAttempt,
102
+ gate_result: {kind: 'not_evaluated'},
103
+ });
104
+ const evaluationError = stepAttemptDtoSchema.parse({
105
+ ...baseAttempt,
106
+ gate_result: {
107
+ kind: 'evaluation_error',
108
+ reason: 'gate expression evaluation failed',
109
+ exit_code: 1,
110
+ },
111
+ });
112
+
113
+ expect(notEvaluated.gate_result).toEqual({kind: 'not_evaluated'});
114
+ expect(evaluationError.gate_result).toEqual({
115
+ kind: 'evaluation_error',
116
+ reason: 'gate expression evaluation failed',
117
+ exit_code: 1,
118
+ });
119
+ });
120
+
121
+ it('accepts typed gate results and restart feedback', () => {
122
+ const attempt = {
123
+ ...baseAttempt,
124
+ status: 'failed',
125
+ exit_code: 1,
126
+ gate_result: {
127
+ kind: 'failed',
128
+ passed: false,
129
+ source: 'exit_code == 0',
130
+ exit_code: 1,
131
+ },
132
+ restart_feedback: 'gate condition not met',
133
+ };
134
+
135
+ const result = stepAttemptDtoSchema.parse(attempt);
136
+
137
+ expect(result.gate_result).toEqual({
138
+ kind: 'failed',
139
+ passed: false,
140
+ source: 'exit_code == 0',
141
+ exit_code: 1,
142
+ });
143
+ expect(result.restart_feedback).toBe('gate condition not met');
144
+ });
145
+
146
+ it('accepts an explicit unknown gate result for legacy data', () => {
147
+ const attempt = {
148
+ ...baseAttempt,
149
+ gate_result: {
150
+ kind: 'unknown',
151
+ data: {passed: 'yes'},
152
+ },
153
+ };
154
+
155
+ const result = stepAttemptDtoSchema.parse(attempt);
156
+
157
+ expect(result.gate_result).toEqual({
158
+ kind: 'unknown',
159
+ data: {passed: 'yes'},
160
+ });
161
+ });
162
+
163
+ it('rejects inconsistent typed gate results', () => {
164
+ const result = stepAttemptDtoSchema.safeParse({
165
+ ...baseAttempt,
166
+ gate_result: {
167
+ kind: 'passed',
168
+ passed: false,
169
+ source: 'exit_code == 0',
170
+ exit_code: 1,
171
+ },
172
+ });
173
+
174
+ expect(result.success).toBe(false);
175
+ });
176
+ });
@@ -0,0 +1,163 @@
1
+ import {z} from 'zod';
2
+
3
+ // Machine-readable cause of a step failure, for DB troubleshooting. The runner
4
+ // reports it and the server stores it as-is. The `checkout_*`, `git_unavailable`,
5
+ // `workspace_prep_failed`, and `setup_aborted` values cover setup-phase failures.
6
+ // For agent steps the cause is split: `agent_config_invalid` is a user-fixable
7
+ // configuration error (unknown provider, missing provider credentials on the runner,
8
+ // wrong provider/model pair, missing model or prompt), while `agent_invocation_failed`
9
+ // covers a genuine provider/API failure once the config is valid (network, 5xx, auth
10
+ // rejected at call time). (Aborts are never reported: the step loop stops before reporting.)
11
+ export const stepErrorReasonSchema = z.enum([
12
+ 'checkout_failed',
13
+ 'checkout_auth_failed',
14
+ 'checkout_unavailable',
15
+ 'git_unavailable',
16
+ 'workspace_prep_failed',
17
+ 'setup_aborted',
18
+ 'config_unresolvable',
19
+ 'output_invalid',
20
+ 'agent_config_invalid',
21
+ 'agent_invocation_failed',
22
+ ]);
23
+
24
+ export type StepErrorReasonDto = z.infer<typeof stepErrorReasonSchema>;
25
+
26
+ export const agentConfigIssueSchema = z.enum([
27
+ 'step_config_invalid',
28
+ 'provider_not_configured',
29
+ 'provider_unsupported',
30
+ 'model_unavailable',
31
+ 'credentials_invalid',
32
+ ]);
33
+
34
+ export type AgentConfigIssueDto = z.infer<typeof agentConfigIssueSchema>;
35
+
36
+ // Whether a failure is infrastructure (`setup`) or user-code (`user`). Server-derived
37
+ // from the step's type on the read path; the runner never sends it.
38
+ export const stepErrorCategorySchema = z.enum(['setup', 'user']);
39
+
40
+ export type StepErrorCategoryDto = z.infer<typeof stepErrorCategorySchema>;
41
+
42
+ export const STEP_ERROR_MESSAGE_MAX_LENGTH = 2048;
43
+
44
+ export const stepErrorDtoSchema = z
45
+ .object({
46
+ message: z.string().max(STEP_ERROR_MESSAGE_MAX_LENGTH),
47
+ exit_code: z.number().int().nullable().optional(),
48
+ signal: z.string().optional(),
49
+ reason: stepErrorReasonSchema.optional(),
50
+ field: z.string().optional(),
51
+ source: z.string().optional(),
52
+ agent_config_issue: agentConfigIssueSchema.optional(),
53
+ category: stepErrorCategorySchema.optional(),
54
+ })
55
+ .refine(
56
+ (error) => error.agent_config_issue === undefined || error.reason === 'agent_config_invalid',
57
+ {
58
+ message: 'agent_config_issue requires reason to be agent_config_invalid',
59
+ path: ['agent_config_issue'],
60
+ },
61
+ )
62
+ .nullable();
63
+
64
+ export type StepErrorDto = z.infer<typeof stepErrorDtoSchema>;
65
+
66
+ export const stepSourceLocationSchema = z
67
+ .object({
68
+ start_line: z.number().int().positive(),
69
+ end_line: z.number().int().positive(),
70
+ })
71
+ .refine((value) => value.end_line >= value.start_line, {
72
+ message: 'end_line must be greater than or equal to start_line',
73
+ path: ['end_line'],
74
+ });
75
+
76
+ export type StepSourceLocationDto = z.infer<typeof stepSourceLocationSchema>;
77
+
78
+ export const stepDtoSchema = z.object({
79
+ id: z.string().uuid(),
80
+ job_execution_id: z.string().uuid(),
81
+ key: z.string().nullable(),
82
+ name: z.string(),
83
+ source_location: stepSourceLocationSchema.nullable(),
84
+ status: z.string(),
85
+ type: z.string(),
86
+ config: z.record(z.string(), z.unknown()),
87
+ error: stepErrorDtoSchema,
88
+ position: z.number(),
89
+ // Execution-attempt identity of the current projection (>1 after a restart).
90
+ current_attempt: z.number().int(),
91
+ created_at: z.string(),
92
+ updated_at: z.string(),
93
+ });
94
+
95
+ export type StepDto = z.infer<typeof stepDtoSchema>;
96
+
97
+ export const stepGateResultDtoSchema = z
98
+ .discriminatedUnion('kind', [
99
+ z.object({
100
+ kind: z.literal('none'),
101
+ }),
102
+ z.object({
103
+ kind: z.literal('not_evaluated'),
104
+ }),
105
+ z.object({
106
+ kind: z.literal('passed'),
107
+ passed: z.literal(true),
108
+ source: z.string(),
109
+ exit_code: z.number().int().nullable(),
110
+ }),
111
+ z.object({
112
+ kind: z.literal('failed'),
113
+ passed: z.literal(false),
114
+ source: z.string(),
115
+ exit_code: z.number().int().nullable(),
116
+ }),
117
+ z.object({
118
+ kind: z.literal('uncheckable'),
119
+ passed: z.literal(false),
120
+ uncheckable: z.literal(true),
121
+ reason: z.string(),
122
+ exit_code: z.number().int().nullable(),
123
+ }),
124
+ z.object({
125
+ kind: z.literal('evaluation_error'),
126
+ reason: z.string(),
127
+ exit_code: z.number().int().nullable(),
128
+ }),
129
+ z.object({
130
+ kind: z.literal('unknown'),
131
+ data: z.record(z.string(), z.unknown()),
132
+ }),
133
+ ])
134
+ .nullable();
135
+
136
+ export type StepGateResultDto = z.infer<typeof stepGateResultDtoSchema>;
137
+
138
+ // One execution attempt of a step (the durable history behind the current
139
+ // projection). Surfaced in run details so a restarted step's attempts are visible.
140
+ export const stepAttemptDtoSchema = z.object({
141
+ id: z.string().uuid(),
142
+ step_id: z.string().uuid(),
143
+ attempt: z.number().int().positive(),
144
+ execution_order: z.number().int().positive(),
145
+ status: z.string(),
146
+ exit_code: z.number().int().nullable(),
147
+ // `output` and `error` are opaque audit blobs: the raw jsonb persisted for the
148
+ // attempt, NOT snake_case-normalized (their nested keys may be camelCase, e.g.
149
+ // `error.exitCode`). Consume the top-level snake_case `exit_code` for the
150
+ // numeric code; treat these as display/debug payloads.
151
+ output: z.record(z.string(), z.unknown()).nullable(),
152
+ outputs: z.record(z.string(), z.unknown()).nullable(),
153
+ response: z.string().nullable(),
154
+ error: z.record(z.string(), z.unknown()).nullable(),
155
+ // `unknown.data` is the raw jsonb gate payload for legacy or unrecognized
156
+ // rows; nested keys are not snake_case-normalized.
157
+ gate_result: stepGateResultDtoSchema,
158
+ restart_feedback: z.string().nullable(),
159
+ started_at: z.string(),
160
+ finished_at: z.string().nullable(),
161
+ });
162
+
163
+ export type StepAttemptDto = z.infer<typeof stepAttemptDtoSchema>;
@@ -0,0 +1,67 @@
1
+ import {z} from 'zod';
2
+ import {jobDtoSchema} from './job.js';
3
+ import {workflowExecutionEventSchema} from './job-listening.js';
4
+ import {stepAttemptDtoSchema, stepDtoSchema} from './step.js';
5
+ import {workflowRunAttemptDtoSchema, workflowRunResponseSchema} from './workflow-run.js';
6
+
7
+ export const jobExecutionStatusSchema = z.enum([
8
+ 'pending',
9
+ 'running',
10
+ 'succeeded',
11
+ 'failed',
12
+ 'cancelled',
13
+ ]);
14
+
15
+ export const jobExecutionDtoSchema = z.object({
16
+ id: z.string().uuid(),
17
+ job_id: z.string().uuid(),
18
+ sequence: z.number().int().positive(),
19
+ name: z.string(),
20
+ status: jobExecutionStatusSchema,
21
+ status_reason: z.string().nullable(),
22
+ trigger_events: z.array(workflowExecutionEventSchema).default([]),
23
+ outputs: z.record(z.string(), z.unknown()).nullable(),
24
+ queued_at: z.string().nullable(),
25
+ started_at: z.string().nullable(),
26
+ finished_at: z.string().nullable(),
27
+ timed_out_at: z.string().nullable(),
28
+ created_at: z.string(),
29
+ updated_at: z.string(),
30
+ });
31
+
32
+ export type JobExecutionDto = z.infer<typeof jobExecutionDtoSchema>;
33
+
34
+ // A step with its attempt history: one entry per dispatched attempt (a restarted
35
+ // step has more than one). `current_attempt` on the step points at the latest.
36
+ export const workflowRunStepDetailDtoSchema = stepDtoSchema.extend({
37
+ exit_code: z.number().int().nullable(),
38
+ outputs: z.record(z.string(), z.unknown()).nullable(),
39
+ response: z.string().nullable(),
40
+ gate_result: stepAttemptDtoSchema.shape.gate_result,
41
+ attempts: z.array(stepAttemptDtoSchema),
42
+ });
43
+
44
+ export type WorkflowRunStepDetailDto = z.infer<typeof workflowRunStepDetailDtoSchema>;
45
+
46
+ export const workflowRunJobExecutionDetailDtoSchema = jobExecutionDtoSchema.extend({
47
+ steps: z.array(workflowRunStepDetailDtoSchema),
48
+ });
49
+
50
+ export type WorkflowRunJobExecutionDetailDto = z.infer<
51
+ typeof workflowRunJobExecutionDetailDtoSchema
52
+ >;
53
+
54
+ export const workflowRunJobDetailDtoSchema = jobDtoSchema.extend({
55
+ job_executions: z.array(workflowRunJobExecutionDetailDtoSchema),
56
+ });
57
+
58
+ export type WorkflowRunJobDetailDto = z.infer<typeof workflowRunJobDetailDtoSchema>;
59
+
60
+ // The run detail read model returned by `GET /workflows/runs/:id`: a run plus its
61
+ // jobs, each job's steps, and each step's attempt history.
62
+ export const workflowRunDetailResponseSchema = workflowRunResponseSchema.extend({
63
+ run_attempt: workflowRunAttemptDtoSchema,
64
+ jobs: z.array(workflowRunJobDetailDtoSchema),
65
+ });
66
+
67
+ export type WorkflowRunDetailResponseDto = z.infer<typeof workflowRunDetailResponseSchema>;