task-while 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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/bin/task-while.mjs +22 -0
- package/package.json +72 -0
- package/src/agents/claude.ts +175 -0
- package/src/agents/codex.ts +231 -0
- package/src/agents/provider-options.ts +45 -0
- package/src/agents/types.ts +69 -0
- package/src/batch/config.ts +109 -0
- package/src/batch/discovery.ts +35 -0
- package/src/batch/provider.ts +79 -0
- package/src/commands/batch.ts +266 -0
- package/src/commands/run.ts +270 -0
- package/src/core/engine-helpers.ts +114 -0
- package/src/core/engine-outcomes.ts +166 -0
- package/src/core/engine.ts +223 -0
- package/src/core/orchestrator-helpers.ts +52 -0
- package/src/core/orchestrator-integrate-resume.ts +149 -0
- package/src/core/orchestrator-review-resume.ts +228 -0
- package/src/core/orchestrator-task-attempt.ts +257 -0
- package/src/core/orchestrator.ts +99 -0
- package/src/core/runtime.ts +175 -0
- package/src/core/task-topology.ts +85 -0
- package/src/index.ts +121 -0
- package/src/prompts/implementer.ts +18 -0
- package/src/prompts/reviewer.ts +26 -0
- package/src/runtime/fs-runtime.ts +209 -0
- package/src/runtime/git.ts +137 -0
- package/src/runtime/github-pr-snapshot-decode.ts +307 -0
- package/src/runtime/github-pr-snapshot-queries.ts +137 -0
- package/src/runtime/github-pr-snapshot.ts +139 -0
- package/src/runtime/github.ts +232 -0
- package/src/runtime/path-layout.ts +13 -0
- package/src/runtime/workspace-resolver.ts +125 -0
- package/src/schema/index.ts +127 -0
- package/src/schema/model.ts +233 -0
- package/src/schema/shared.ts +93 -0
- package/src/task-sources/openspec/cli-json.ts +79 -0
- package/src/task-sources/openspec/context-files.ts +121 -0
- package/src/task-sources/openspec/parse-tasks-md.ts +57 -0
- package/src/task-sources/openspec/session.ts +235 -0
- package/src/task-sources/openspec/source.ts +59 -0
- package/src/task-sources/registry.ts +22 -0
- package/src/task-sources/spec-kit/parse-tasks-md.ts +48 -0
- package/src/task-sources/spec-kit/session.ts +174 -0
- package/src/task-sources/spec-kit/source.ts +30 -0
- package/src/task-sources/types.ts +47 -0
- package/src/types.ts +29 -0
- package/src/utils/fs.ts +31 -0
- package/src/workflow/config.ts +127 -0
- package/src/workflow/direct-preset.ts +44 -0
- package/src/workflow/finalize-task-checkbox.ts +24 -0
- package/src/workflow/preset.ts +86 -0
- package/src/workflow/pull-request-preset.ts +312 -0
- package/src/workflow/remote-reviewer.ts +243 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { buildImplementerPrompt } from '../prompts/implementer'
|
|
2
|
+
import { buildReviewerPrompt } from '../prompts/reviewer'
|
|
3
|
+
import {
|
|
4
|
+
implementOutputSchema,
|
|
5
|
+
reviewOutputSchema,
|
|
6
|
+
validateImplementOutput,
|
|
7
|
+
validateReviewOutput,
|
|
8
|
+
} from '../schema/index'
|
|
9
|
+
|
|
10
|
+
import type { CodexProviderOptions } from './provider-options'
|
|
11
|
+
import type {
|
|
12
|
+
ImplementAgentInput,
|
|
13
|
+
ImplementerProvider,
|
|
14
|
+
ReviewAgentInput,
|
|
15
|
+
ReviewerProvider,
|
|
16
|
+
} from './types'
|
|
17
|
+
|
|
18
|
+
export interface CodexRunResult {
|
|
19
|
+
finalResponse: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CodexUsage {
|
|
23
|
+
cached_input_tokens: number
|
|
24
|
+
input_tokens: number
|
|
25
|
+
output_tokens: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CodexTurnFailedEvent {
|
|
29
|
+
error: CodexTurnFailedError
|
|
30
|
+
type: 'turn.failed'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CodexItemEvent {
|
|
34
|
+
item: CodexItemPayload
|
|
35
|
+
type: 'item.completed' | 'item.started' | 'item.updated'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CodexErrorEvent {
|
|
39
|
+
message: string
|
|
40
|
+
type: 'error'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CodexThreadStartedEvent {
|
|
44
|
+
thread_id: string
|
|
45
|
+
type: 'thread.started'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CodexTurnCompletedEvent {
|
|
49
|
+
type: 'turn.completed'
|
|
50
|
+
usage: CodexUsage
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CodexTurnStartedEvent {
|
|
54
|
+
type: 'turn.started'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CodexTurnFailedError {
|
|
58
|
+
message: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CodexItemPayload {
|
|
62
|
+
text?: string
|
|
63
|
+
type: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type CodexThreadEvent =
|
|
67
|
+
| CodexErrorEvent
|
|
68
|
+
| CodexItemEvent
|
|
69
|
+
| CodexThreadStartedEvent
|
|
70
|
+
| CodexTurnCompletedEvent
|
|
71
|
+
| CodexTurnFailedEvent
|
|
72
|
+
| CodexTurnStartedEvent
|
|
73
|
+
|
|
74
|
+
export type CodexThreadEventHandler = (event: CodexThreadEvent) => void
|
|
75
|
+
|
|
76
|
+
export interface CodexRunStreamedResult {
|
|
77
|
+
events: AsyncGenerator<CodexThreadEvent>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CodexThreadRunOptions {
|
|
81
|
+
outputSchema: Record<string, unknown>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CodexThreadLike {
|
|
85
|
+
run: (
|
|
86
|
+
prompt: string,
|
|
87
|
+
options: CodexThreadRunOptions,
|
|
88
|
+
) => Promise<CodexRunResult>
|
|
89
|
+
runStreamed?: (
|
|
90
|
+
prompt: string,
|
|
91
|
+
options: CodexThreadRunOptions,
|
|
92
|
+
) => Promise<CodexRunStreamedResult>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CodexStartThreadOptions {
|
|
96
|
+
model?: string
|
|
97
|
+
modelReasoningEffort?: 'high' | 'low' | 'medium' | 'minimal' | 'xhigh'
|
|
98
|
+
workingDirectory: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface CodexClientLike {
|
|
102
|
+
startThread: (options: CodexStartThreadOptions) => CodexThreadLike
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface CodexAgentClientOptions extends CodexProviderOptions {
|
|
106
|
+
onEvent?: CodexThreadEventHandler
|
|
107
|
+
workspaceRoot: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface CodexStructuredInput {
|
|
111
|
+
outputSchema: Record<string, unknown>
|
|
112
|
+
prompt: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function defaultClientFactory(): Promise<CodexClientLike> {
|
|
116
|
+
const { Codex } = await import('@openai/codex-sdk')
|
|
117
|
+
return new Codex()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class CodexAgentClient implements ImplementerProvider, ReviewerProvider {
|
|
121
|
+
private clientPromise: null | Promise<CodexClientLike> = null
|
|
122
|
+
public readonly name = 'codex'
|
|
123
|
+
|
|
124
|
+
public constructor(private readonly options: CodexAgentClientOptions) {}
|
|
125
|
+
|
|
126
|
+
private async collectStreamedTurn<T>(
|
|
127
|
+
thread: CodexThreadLike,
|
|
128
|
+
input: CodexStructuredInput,
|
|
129
|
+
): Promise<T> {
|
|
130
|
+
const streamedTurn = await thread.runStreamed!(input.prompt, {
|
|
131
|
+
outputSchema: input.outputSchema,
|
|
132
|
+
})
|
|
133
|
+
let finalResponse = ''
|
|
134
|
+
|
|
135
|
+
for await (const event of streamedTurn.events) {
|
|
136
|
+
this.options.onEvent?.(event)
|
|
137
|
+
|
|
138
|
+
if (event.type === 'error') {
|
|
139
|
+
throw new Error(event.message)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (event.type === 'turn.failed') {
|
|
143
|
+
throw new Error(event.error.message)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
event.type === 'item.completed' &&
|
|
148
|
+
event.item.type === 'agent_message'
|
|
149
|
+
) {
|
|
150
|
+
finalResponse = event.item.text?.trim() ?? ''
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!finalResponse) {
|
|
155
|
+
throw new Error('Codex agent client returned empty finalResponse')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(finalResponse) as T
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new Error('Codex agent client returned non-JSON finalResponse', {
|
|
162
|
+
cause: error,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async getClient(): Promise<CodexClientLike> {
|
|
168
|
+
this.clientPromise ??= defaultClientFactory()
|
|
169
|
+
return this.clientPromise
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public async implement(input: ImplementAgentInput) {
|
|
173
|
+
const prompt = await buildImplementerPrompt(input)
|
|
174
|
+
const output = await this.invokeStructured<unknown>({
|
|
175
|
+
outputSchema: implementOutputSchema,
|
|
176
|
+
prompt,
|
|
177
|
+
})
|
|
178
|
+
return validateImplementOutput(output)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public async invokeStructured<T>(input: CodexStructuredInput): Promise<T> {
|
|
182
|
+
const client = await this.getClient()
|
|
183
|
+
const startThreadOptions: CodexStartThreadOptions = {
|
|
184
|
+
workingDirectory: this.options.workspaceRoot,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this.options.model) {
|
|
188
|
+
startThreadOptions.model = this.options.model
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.options.effort) {
|
|
192
|
+
startThreadOptions.modelReasoningEffort = this.options.effort
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const thread = client.startThread(startThreadOptions)
|
|
196
|
+
|
|
197
|
+
if (this.options.onEvent && thread.runStreamed) {
|
|
198
|
+
return this.collectStreamedTurn<T>(thread, input)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const turn = await thread.run(input.prompt, {
|
|
202
|
+
outputSchema: input.outputSchema,
|
|
203
|
+
})
|
|
204
|
+
const response = turn.finalResponse.trim()
|
|
205
|
+
if (!response) {
|
|
206
|
+
throw new Error('Codex agent client returned empty finalResponse')
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(response) as T
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new Error('Codex agent client returned non-JSON finalResponse', {
|
|
212
|
+
cause: error,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public async review(input: ReviewAgentInput) {
|
|
218
|
+
const prompt = await buildReviewerPrompt(input)
|
|
219
|
+
const output = await this.invokeStructured<unknown>({
|
|
220
|
+
outputSchema: reviewOutputSchema,
|
|
221
|
+
prompt,
|
|
222
|
+
})
|
|
223
|
+
return validateReviewOutput(output)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function createCodexProvider(
|
|
228
|
+
options: CodexAgentClientOptions,
|
|
229
|
+
): ImplementerProvider & ReviewerProvider {
|
|
230
|
+
return new CodexAgentClient(options)
|
|
231
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const modelSchema = z.string().trim().min(1)
|
|
4
|
+
|
|
5
|
+
export const codexEffortSchema = z.enum([
|
|
6
|
+
'minimal',
|
|
7
|
+
'low',
|
|
8
|
+
'medium',
|
|
9
|
+
'high',
|
|
10
|
+
'xhigh',
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
export const claudeEffortSchema = z.enum(['low', 'medium', 'high', 'max'])
|
|
14
|
+
|
|
15
|
+
export const codexProviderOptionsSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
effort: codexEffortSchema.optional(),
|
|
18
|
+
model: modelSchema.optional(),
|
|
19
|
+
})
|
|
20
|
+
.strict()
|
|
21
|
+
|
|
22
|
+
export const claudeProviderOptionsSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
effort: claudeEffortSchema.optional(),
|
|
25
|
+
model: modelSchema.optional(),
|
|
26
|
+
})
|
|
27
|
+
.strict()
|
|
28
|
+
|
|
29
|
+
export type CodexProviderOptions = z.infer<typeof codexProviderOptionsSchema>
|
|
30
|
+
export type ClaudeProviderOptions = z.infer<typeof claudeProviderOptionsSchema>
|
|
31
|
+
|
|
32
|
+
export type WorkflowRoleProviderOptions =
|
|
33
|
+
| (ClaudeProviderOptions & { provider: 'claude' })
|
|
34
|
+
| (CodexProviderOptions & { provider: 'codex' })
|
|
35
|
+
|
|
36
|
+
export function providerOptionsEqual(
|
|
37
|
+
left: WorkflowRoleProviderOptions,
|
|
38
|
+
right: WorkflowRoleProviderOptions,
|
|
39
|
+
) {
|
|
40
|
+
return (
|
|
41
|
+
left.provider === right.provider &&
|
|
42
|
+
left.model === right.model &&
|
|
43
|
+
left.effort === right.effort
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { PullRequestSnapshot } from '../core/runtime'
|
|
2
|
+
import type { TaskPrompt } from '../task-sources/types'
|
|
3
|
+
import type { ImplementOutput, ReviewFinding, ReviewOutput } from '../types'
|
|
4
|
+
|
|
5
|
+
export interface ImplementAgentInput {
|
|
6
|
+
attempt: number
|
|
7
|
+
generation: number
|
|
8
|
+
lastFindings: ReviewFinding[]
|
|
9
|
+
prompt: TaskPrompt
|
|
10
|
+
taskHandle: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ReviewAgentInput {
|
|
14
|
+
actualChangedFiles: string[]
|
|
15
|
+
attempt: number
|
|
16
|
+
generation: number
|
|
17
|
+
implement: ImplementOutput
|
|
18
|
+
lastFindings: ReviewFinding[]
|
|
19
|
+
prompt: TaskPrompt
|
|
20
|
+
taskHandle: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ImplementerProvider {
|
|
24
|
+
implement: (input: ImplementAgentInput) => Promise<ImplementOutput>
|
|
25
|
+
readonly name: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReviewerProvider {
|
|
29
|
+
readonly name: string
|
|
30
|
+
review: (input: ReviewAgentInput) => Promise<ReviewOutput>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PullRequestReviewInput {
|
|
34
|
+
checkpointStartedAt: string
|
|
35
|
+
completionCriteria: string[]
|
|
36
|
+
pullRequest: PullRequestSnapshot
|
|
37
|
+
taskHandle: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PullRequestReviewApprovedResult {
|
|
41
|
+
kind: 'approved'
|
|
42
|
+
review: ReviewOutput
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PullRequestReviewPendingResult {
|
|
46
|
+
kind: 'pending'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PullRequestReviewRejectedResult {
|
|
50
|
+
kind: 'rejected'
|
|
51
|
+
review: ReviewOutput
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type PullRequestReviewResult =
|
|
55
|
+
| PullRequestReviewApprovedResult
|
|
56
|
+
| PullRequestReviewPendingResult
|
|
57
|
+
| PullRequestReviewRejectedResult
|
|
58
|
+
|
|
59
|
+
export interface RemoteReviewerProvider {
|
|
60
|
+
evaluatePullRequestReview: (
|
|
61
|
+
input: PullRequestReviewInput,
|
|
62
|
+
) => Promise<PullRequestReviewResult>
|
|
63
|
+
readonly name: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface WorkflowRoleProviders {
|
|
67
|
+
implementer: ImplementerProvider
|
|
68
|
+
reviewer: RemoteReviewerProvider | ReviewerProvider
|
|
69
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { parse } from 'yaml'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
claudeProviderOptionsSchema,
|
|
9
|
+
codexProviderOptionsSchema,
|
|
10
|
+
type WorkflowRoleProviderOptions,
|
|
11
|
+
} from '../agents/provider-options'
|
|
12
|
+
|
|
13
|
+
export const batchProviderSchema = z.enum(['claude', 'codex'])
|
|
14
|
+
|
|
15
|
+
const jsonSchemaSchema = z.custom<Record<string, unknown>>(
|
|
16
|
+
(value) =>
|
|
17
|
+
typeof value === 'object' && value !== null && !Array.isArray(value),
|
|
18
|
+
{
|
|
19
|
+
message: 'schema must be an object',
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const globEntrySchema = z.string().trim().min(1)
|
|
24
|
+
|
|
25
|
+
const globSchema = z
|
|
26
|
+
.union([globEntrySchema, z.array(globEntrySchema).min(1)])
|
|
27
|
+
.optional()
|
|
28
|
+
|
|
29
|
+
const commonBatchConfigSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
glob: globSchema,
|
|
32
|
+
prompt: z.string().trim().min(1),
|
|
33
|
+
schema: jsonSchemaSchema,
|
|
34
|
+
})
|
|
35
|
+
.strict()
|
|
36
|
+
|
|
37
|
+
const batchConfigSchema = z.discriminatedUnion('provider', [
|
|
38
|
+
z
|
|
39
|
+
.object({
|
|
40
|
+
provider: z.literal('claude'),
|
|
41
|
+
})
|
|
42
|
+
.extend(claudeProviderOptionsSchema.shape)
|
|
43
|
+
.extend(commonBatchConfigSchema.shape)
|
|
44
|
+
.strict(),
|
|
45
|
+
z
|
|
46
|
+
.object({
|
|
47
|
+
provider: z.literal('codex'),
|
|
48
|
+
})
|
|
49
|
+
.extend(codexProviderOptionsSchema.shape)
|
|
50
|
+
.extend(commonBatchConfigSchema.shape)
|
|
51
|
+
.strict(),
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
export type BatchProviderName = WorkflowRoleProviderOptions['provider']
|
|
55
|
+
|
|
56
|
+
interface BatchConfigBase {
|
|
57
|
+
configDir: string
|
|
58
|
+
configPath: string
|
|
59
|
+
glob: string[]
|
|
60
|
+
prompt: string
|
|
61
|
+
schema: Record<string, unknown>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type BatchConfig = BatchConfigBase & WorkflowRoleProviderOptions
|
|
65
|
+
|
|
66
|
+
export interface LoadBatchConfigInput {
|
|
67
|
+
configPath: string
|
|
68
|
+
cwd: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeGlobConfig(glob: string | string[] | undefined) {
|
|
72
|
+
if (!glob) {
|
|
73
|
+
return ['**/*']
|
|
74
|
+
}
|
|
75
|
+
return typeof glob === 'string' ? [glob] : glob
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function loadBatchConfig(
|
|
79
|
+
input: LoadBatchConfigInput,
|
|
80
|
+
): Promise<BatchConfig> {
|
|
81
|
+
const configPath = path.resolve(input.cwd, input.configPath)
|
|
82
|
+
const configSource = await readFile(configPath, 'utf8')
|
|
83
|
+
const rawConfig = parse(configSource) ?? {}
|
|
84
|
+
const parsedConfig = batchConfigSchema.parse(rawConfig)
|
|
85
|
+
const configDir = path.dirname(configPath)
|
|
86
|
+
const baseConfig = {
|
|
87
|
+
configDir,
|
|
88
|
+
configPath,
|
|
89
|
+
glob: normalizeGlobConfig(parsedConfig.glob),
|
|
90
|
+
prompt: parsedConfig.prompt,
|
|
91
|
+
schema: parsedConfig.schema,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (parsedConfig.provider === 'claude') {
|
|
95
|
+
return {
|
|
96
|
+
...baseConfig,
|
|
97
|
+
provider: 'claude',
|
|
98
|
+
...(parsedConfig.model ? { model: parsedConfig.model } : {}),
|
|
99
|
+
...(parsedConfig.effort ? { effort: parsedConfig.effort } : {}),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...baseConfig,
|
|
105
|
+
provider: 'codex',
|
|
106
|
+
...(parsedConfig.model ? { model: parsedConfig.model } : {}),
|
|
107
|
+
...(parsedConfig.effort ? { effort: parsedConfig.effort } : {}),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { glob } from 'glob'
|
|
4
|
+
|
|
5
|
+
export interface DiscoverBatchFilesInput {
|
|
6
|
+
baseDir: string
|
|
7
|
+
excludedFiles: Set<string>
|
|
8
|
+
patterns: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeBatchPath(value: string) {
|
|
12
|
+
return value.split(path.sep).join('/')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function discoverBatchFiles(
|
|
16
|
+
input: DiscoverBatchFilesInput,
|
|
17
|
+
): Promise<string[]> {
|
|
18
|
+
const excluded = new Set(
|
|
19
|
+
[...input.excludedFiles].map((filePath) =>
|
|
20
|
+
normalizeBatchPath(path.relative(input.baseDir, filePath)),
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
const matched = await glob(input.patterns, {
|
|
24
|
+
absolute: false,
|
|
25
|
+
cwd: input.baseDir,
|
|
26
|
+
dot: true,
|
|
27
|
+
ignore: ['.git/**', 'node_modules/**'],
|
|
28
|
+
nodir: true,
|
|
29
|
+
posix: true,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return [...new Set(matched.map(normalizeBatchPath))]
|
|
33
|
+
.filter((filePath) => !excluded.has(filePath))
|
|
34
|
+
.sort((left, right) => left.localeCompare(right))
|
|
35
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ClaudeAgentClient } from '../agents/claude'
|
|
2
|
+
import { CodexAgentClient } from '../agents/codex'
|
|
3
|
+
|
|
4
|
+
import type { WorkflowRoleProviderOptions } from '../agents/provider-options'
|
|
5
|
+
|
|
6
|
+
export interface BatchFileInput {
|
|
7
|
+
content: string
|
|
8
|
+
filePath: string
|
|
9
|
+
outputSchema: Record<string, unknown>
|
|
10
|
+
prompt: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BatchStructuredOutputProvider {
|
|
14
|
+
readonly name: string
|
|
15
|
+
runFile: (input: BatchFileInput) => Promise<unknown>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CreateBatchStructuredOutputProviderInput =
|
|
19
|
+
WorkflowRoleProviderOptions & {
|
|
20
|
+
workspaceRoot: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildBatchPrompt(input: BatchFileInput) {
|
|
24
|
+
return [
|
|
25
|
+
'Process exactly one file and return structured output only.',
|
|
26
|
+
input.prompt,
|
|
27
|
+
`File path: ${input.filePath}`,
|
|
28
|
+
'File content:',
|
|
29
|
+
input.content,
|
|
30
|
+
].join('\n\n')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class CodexBatchStructuredOutputProvider implements BatchStructuredOutputProvider {
|
|
34
|
+
public readonly name = 'codex'
|
|
35
|
+
|
|
36
|
+
public constructor(private readonly client: CodexAgentClient) {}
|
|
37
|
+
|
|
38
|
+
public async runFile(input: BatchFileInput) {
|
|
39
|
+
return this.client.invokeStructured<unknown>({
|
|
40
|
+
outputSchema: input.outputSchema,
|
|
41
|
+
prompt: buildBatchPrompt(input),
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class ClaudeBatchStructuredOutputProvider implements BatchStructuredOutputProvider {
|
|
47
|
+
public readonly name = 'claude'
|
|
48
|
+
|
|
49
|
+
public constructor(private readonly client: ClaudeAgentClient) {}
|
|
50
|
+
|
|
51
|
+
public async runFile(input: BatchFileInput) {
|
|
52
|
+
return this.client.invokeStructured<unknown>({
|
|
53
|
+
outputSchema: input.outputSchema,
|
|
54
|
+
prompt: buildBatchPrompt(input),
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createBatchStructuredOutputProvider(
|
|
60
|
+
input: CreateBatchStructuredOutputProviderInput,
|
|
61
|
+
): BatchStructuredOutputProvider {
|
|
62
|
+
if (input.provider === 'codex') {
|
|
63
|
+
return new CodexBatchStructuredOutputProvider(
|
|
64
|
+
new CodexAgentClient({
|
|
65
|
+
...(input.effort ? { effort: input.effort } : {}),
|
|
66
|
+
...(input.model ? { model: input.model } : {}),
|
|
67
|
+
workspaceRoot: input.workspaceRoot,
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return new ClaudeBatchStructuredOutputProvider(
|
|
73
|
+
new ClaudeAgentClient({
|
|
74
|
+
...(input.effort ? { effort: input.effort } : {}),
|
|
75
|
+
...(input.model ? { model: input.model } : {}),
|
|
76
|
+
workspaceRoot: input.workspaceRoot,
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
}
|