task-while 0.0.3 → 0.0.5
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/README.md +11 -4
- package/bin/task-while.mjs +0 -0
- package/package.json +18 -19
- package/src/agents/claude.ts +35 -27
- package/src/agents/codex.ts +31 -16
- package/src/agents/provider-options.ts +17 -0
- package/src/agents/timeout.ts +41 -0
- package/src/batch/config.ts +2 -0
- package/src/batch/provider.ts +2 -0
- package/src/commands/batch.ts +100 -0
- package/src/commands/run-providers.ts +7 -6
- package/src/core/create-runtime-ports.ts +4 -1
- package/src/core/runtime.ts +1 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ pnpm exec task-while run
|
|
|
40
40
|
|
|
41
41
|
## Configuration
|
|
42
42
|
|
|
43
|
-
`while.yaml` configures the `run` workflow only. When it is absent, the CLI runs `task.source: spec-kit`, `task.maxIterations: 5`, and `workflow.mode: direct` with `codex` for both roles. Each workflow role accepts provider-specific `model` and `
|
|
43
|
+
`while.yaml` configures the `run` workflow only. When it is absent, the CLI runs `task.source: spec-kit`, `task.maxIterations: 5`, and `workflow.mode: direct` with `codex` for both roles. Each workflow role accepts provider-specific `model`, `effort`, and optional `timeout` in milliseconds.
|
|
44
44
|
|
|
45
45
|
```yaml
|
|
46
46
|
task:
|
|
@@ -53,20 +53,23 @@ workflow:
|
|
|
53
53
|
implementer:
|
|
54
54
|
model: gpt-5-codex
|
|
55
55
|
effort: high
|
|
56
|
+
timeout: 900000
|
|
56
57
|
reviewer:
|
|
57
58
|
model: gpt-5-codex
|
|
58
59
|
effort: high
|
|
60
|
+
timeout: 900000
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
Current status:
|
|
62
64
|
|
|
63
65
|
- `workflow.roles.<role>.provider` accepts `codex` or `claude`; when omitted it defaults to `codex`, including roles that only set `model` and/or `effort`
|
|
66
|
+
- `workflow.roles.<role>.timeout` is optional and sets a per-turn timeout in milliseconds for local agent runs; valid values are positive integers up to `2147483647`
|
|
64
67
|
- `codex` `effort` accepts `minimal`, `low`, `medium`, `high`, or `xhigh`
|
|
65
68
|
- `claude` `effort` accepts `low`, `medium`, `high`, or `max`
|
|
66
69
|
- `workflow.mode: direct` requires `implementer` and `reviewer` to use identical `model` and `effort` when they share the same provider
|
|
67
70
|
- `workflow.mode: direct` uses a local reviewer
|
|
68
71
|
- `workflow.mode: pull-request` pushes a task branch, polls GitHub PR review from `chatgpt-codex-connector[bot]`, then squash-merges on approval
|
|
69
|
-
- in `workflow.mode: pull-request`, reviewer `provider` still selects the remote reviewer, but any local reviewer `model` and `
|
|
72
|
+
- in `workflow.mode: pull-request`, reviewer `provider` still selects the remote reviewer, but any local reviewer `model`, `effort`, and `timeout` values are ignored
|
|
70
73
|
- `workflow.mode: pull-request` currently supports only `codex` as the remote reviewer provider
|
|
71
74
|
- `task.maxIterations` uses the same configured limit for every task in the selected source session; run workflow retries share a single per-task budget across phases
|
|
72
75
|
|
|
@@ -127,12 +130,15 @@ cd /path/to/workspace
|
|
|
127
130
|
pnpm exec task-while batch --config ./batch.yaml
|
|
128
131
|
```
|
|
129
132
|
|
|
133
|
+
This repository also includes a repo-local skill at `skills/generate-batch-yaml/` for generating batch configs from natural-language requirements.
|
|
134
|
+
|
|
130
135
|
Batch config example:
|
|
131
136
|
|
|
132
137
|
```yaml
|
|
133
138
|
provider: claude
|
|
134
139
|
model: claude-sonnet-4-6
|
|
135
140
|
effort: max
|
|
141
|
+
timeout: 300000
|
|
136
142
|
glob:
|
|
137
143
|
- 'src/**/*.{ts,tsx}'
|
|
138
144
|
prompt: |
|
|
@@ -155,15 +161,16 @@ Batch behavior:
|
|
|
155
161
|
- `glob` is optional and defaults to `**/*`
|
|
156
162
|
- `glob` is resolved relative to the directory that contains `batch.yaml`
|
|
157
163
|
- `provider`, `prompt`, and `schema` are required
|
|
158
|
-
- `model` and `
|
|
164
|
+
- `model`, `effort`, and `timeout` are optional and are forwarded to the selected provider client
|
|
159
165
|
- batch `provider` accepts `codex` or `claude`
|
|
160
166
|
- batch `codex` `effort` accepts `minimal`, `low`, `medium`, `high`, or `xhigh`
|
|
161
167
|
- batch `claude` `effort` accepts `low`, `medium`, `high`, or `max`
|
|
168
|
+
- batch `timeout` is an optional per-file timeout in milliseconds; valid values are positive integers up to `2147483647`
|
|
162
169
|
- each run scans files under the `batch.yaml` directory and filters them by `glob`
|
|
163
170
|
- structured results are written beside the YAML file in `results.json`
|
|
164
171
|
- internal harness state is written under `.while/harness/` beside the YAML file
|
|
165
172
|
- result keys are relative to the directory that contains `batch.yaml`
|
|
166
|
-
- `--verbose` streams direct provider details to `stderr` during batch execution, including Claude init/task/tool/result summaries and Codex thinking, commands, MCP tools, file updates, todo changes, messages, and final usage
|
|
173
|
+
- `--verbose` streams batch-level progress and direct provider details to `stderr` during batch execution, including the current file, completion counts, Claude init/task/tool/result summaries, and Codex thinking, commands, MCP tools, file updates, todo changes, messages, and final usage
|
|
167
174
|
- rerunning the command resumes unfinished work and skips files that already have accepted results
|
|
168
175
|
- failed files are suspended and retried after all pending files are processed
|
|
169
176
|
- file-level retries are limited by `maxRetries` (default 3); exhausted files are marked blocked
|
package/bin/task-while.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-while",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"packageManager": "pnpm@10.32.1",
|
|
3
|
+
"version": "0.0.5",
|
|
5
4
|
"description": "Git-first task orchestrator for task-source workspaces",
|
|
6
5
|
"author": "Zhang Yu",
|
|
7
6
|
"license": "MIT",
|
|
@@ -32,23 +31,9 @@
|
|
|
32
31
|
"engines": {
|
|
33
32
|
"node": ">=24"
|
|
34
33
|
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"dev": "tsx src/index.ts",
|
|
37
|
-
"format": "prettier . --write",
|
|
38
|
-
"format:check": "prettier . --check",
|
|
39
|
-
"lint": "eslint .",
|
|
40
|
-
"lint:fix": "eslint . --fix",
|
|
41
|
-
"smoke:codex": "tsx fixtures/smoke/codex-provider.ts",
|
|
42
|
-
"smoke:e2e:codex": "tsx fixtures/smoke/codex-e2e.ts",
|
|
43
|
-
"smoke:github-pr-snapshot": "tsx fixtures/smoke/github-pr-snapshot.ts",
|
|
44
|
-
"typecheck": "tsc --noEmit",
|
|
45
|
-
"test": "vitest run",
|
|
46
|
-
"test:watch": "vitest",
|
|
47
|
-
"coverage": "vitest run --coverage"
|
|
48
|
-
},
|
|
49
34
|
"dependencies": {
|
|
50
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
51
|
-
"@openai/codex-sdk": "^0.
|
|
35
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.104",
|
|
36
|
+
"@openai/codex-sdk": "^0.120.0",
|
|
52
37
|
"ajv": "^8.18.0",
|
|
53
38
|
"arg": "^5.0.2",
|
|
54
39
|
"execa": "^8.0.1",
|
|
@@ -68,5 +53,19 @@
|
|
|
68
53
|
"prettier": "^3.8.1",
|
|
69
54
|
"typescript": "^5.9.3",
|
|
70
55
|
"vitest": "4.1.0"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"dev": "tsx src/index.ts",
|
|
59
|
+
"format": "prettier . --write",
|
|
60
|
+
"format:check": "prettier . --check",
|
|
61
|
+
"lint": "eslint .",
|
|
62
|
+
"lint:fix": "eslint . --fix",
|
|
63
|
+
"smoke:codex": "tsx fixtures/smoke/codex-provider.ts",
|
|
64
|
+
"smoke:e2e:codex": "tsx fixtures/smoke/codex-e2e.ts",
|
|
65
|
+
"smoke:github-pr-snapshot": "tsx fixtures/smoke/github-pr-snapshot.ts",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"test:watch": "vitest",
|
|
69
|
+
"coverage": "vitest run --coverage"
|
|
71
70
|
}
|
|
72
|
-
}
|
|
71
|
+
}
|
package/src/agents/claude.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
validateImplementOutput,
|
|
7
7
|
validateReviewOutput,
|
|
8
8
|
} from '../schema/index'
|
|
9
|
+
import { withAbortTimeout } from './timeout'
|
|
9
10
|
|
|
10
11
|
import type { Options as ClaudeQueryOptions } from '@anthropic-ai/claude-agent-sdk'
|
|
11
12
|
|
|
@@ -278,34 +279,41 @@ export class ClaudeAgentClient
|
|
|
278
279
|
|
|
279
280
|
public async invokeStructured<T>(input: ClaudeStructuredInput): Promise<T> {
|
|
280
281
|
const { query } = await import('@anthropic-ai/claude-agent-sdk')
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
282
|
+
return withAbortTimeout(
|
|
283
|
+
this.name,
|
|
284
|
+
this.options.timeout,
|
|
285
|
+
async (controller) => {
|
|
286
|
+
const queryOptions = {
|
|
287
|
+
allowDangerouslySkipPermissions: true,
|
|
288
|
+
cwd: this.options.workspaceRoot,
|
|
289
|
+
permissionMode: 'bypassPermissions',
|
|
290
|
+
outputFormat: {
|
|
291
|
+
schema: input.outputSchema,
|
|
292
|
+
type: 'json_schema',
|
|
293
|
+
},
|
|
294
|
+
...(this.options.onEvent
|
|
295
|
+
? {
|
|
296
|
+
agentProgressSummaries: true,
|
|
297
|
+
includePartialMessages: true,
|
|
298
|
+
}
|
|
299
|
+
: {
|
|
300
|
+
includePartialMessages: false,
|
|
301
|
+
}),
|
|
302
|
+
...(this.options.model ? { model: this.options.model } : {}),
|
|
303
|
+
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
304
|
+
...(controller ? { abortController: controller } : {}),
|
|
305
|
+
} satisfies ClaudeQueryOptions
|
|
306
|
+
|
|
307
|
+
const messages = query({
|
|
308
|
+
options: queryOptions,
|
|
309
|
+
prompt: input.prompt,
|
|
310
|
+
})
|
|
305
311
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
312
|
+
return this.collectStructuredOutput(
|
|
313
|
+
messages as AsyncIterable<QueryMessage>,
|
|
314
|
+
) as Promise<T>
|
|
315
|
+
},
|
|
316
|
+
)
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
public async review(input: ReviewAgentInput) {
|
package/src/agents/codex.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
validateImplementOutput,
|
|
7
7
|
validateReviewOutput,
|
|
8
8
|
} from '../schema/index'
|
|
9
|
+
import { withAbortTimeout } from './timeout'
|
|
9
10
|
|
|
10
11
|
import type { CodexProviderOptions } from './provider-options'
|
|
11
12
|
import type {
|
|
@@ -143,6 +144,7 @@ export interface CodexRunStreamedResult {
|
|
|
143
144
|
|
|
144
145
|
export interface CodexThreadRunOptions {
|
|
145
146
|
outputSchema: Record<string, unknown>
|
|
147
|
+
signal?: AbortSignal
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
export interface CodexThreadLike {
|
|
@@ -190,9 +192,11 @@ export class CodexAgentClient implements ImplementerProvider, ReviewerProvider {
|
|
|
190
192
|
private async collectStreamedTurn<T>(
|
|
191
193
|
thread: CodexThreadLike,
|
|
192
194
|
input: CodexStructuredInput,
|
|
195
|
+
signal?: AbortSignal,
|
|
193
196
|
): Promise<T> {
|
|
194
197
|
const streamedTurn = await thread.runStreamed!(input.prompt, {
|
|
195
198
|
outputSchema: input.outputSchema,
|
|
199
|
+
...(signal ? { signal } : {}),
|
|
196
200
|
})
|
|
197
201
|
let finalResponse = ''
|
|
198
202
|
|
|
@@ -259,23 +263,34 @@ export class CodexAgentClient implements ImplementerProvider, ReviewerProvider {
|
|
|
259
263
|
const thread = client.startThread(startThreadOptions)
|
|
260
264
|
|
|
261
265
|
if (this.options.onEvent && thread.runStreamed) {
|
|
262
|
-
return this.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const turn = await thread.run(input.prompt, {
|
|
266
|
-
outputSchema: input.outputSchema,
|
|
267
|
-
})
|
|
268
|
-
const response = turn.finalResponse.trim()
|
|
269
|
-
if (!response) {
|
|
270
|
-
throw new Error('Codex agent client returned empty finalResponse')
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
return JSON.parse(response) as T
|
|
274
|
-
} catch (error) {
|
|
275
|
-
throw new Error('Codex agent client returned non-JSON finalResponse', {
|
|
276
|
-
cause: error,
|
|
277
|
-
})
|
|
266
|
+
return withAbortTimeout(this.name, this.options.timeout, (controller) =>
|
|
267
|
+
this.collectStreamedTurn<T>(thread, input, controller?.signal),
|
|
268
|
+
)
|
|
278
269
|
}
|
|
270
|
+
return withAbortTimeout(
|
|
271
|
+
this.name,
|
|
272
|
+
this.options.timeout,
|
|
273
|
+
async (controller) => {
|
|
274
|
+
const turn = await thread.run(input.prompt, {
|
|
275
|
+
outputSchema: input.outputSchema,
|
|
276
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
277
|
+
})
|
|
278
|
+
const response = turn.finalResponse.trim()
|
|
279
|
+
if (!response) {
|
|
280
|
+
throw new Error('Codex agent client returned empty finalResponse')
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
return JSON.parse(response) as T
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
'Codex agent client returned non-JSON finalResponse',
|
|
287
|
+
{
|
|
288
|
+
cause: error,
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
)
|
|
279
294
|
}
|
|
280
295
|
|
|
281
296
|
public async review(input: ReviewAgentInput) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
3
|
const modelSchema = z.string().trim().min(1)
|
|
4
|
+
const timeoutSchema = z.number().int().positive().max(2_147_483_647)
|
|
4
5
|
|
|
5
6
|
export const codexEffortSchema = z.enum([
|
|
6
7
|
'minimal',
|
|
@@ -16,6 +17,7 @@ export const codexProviderOptionsSchema = z
|
|
|
16
17
|
.object({
|
|
17
18
|
effort: codexEffortSchema.optional(),
|
|
18
19
|
model: modelSchema.optional(),
|
|
20
|
+
timeout: timeoutSchema.optional(),
|
|
19
21
|
})
|
|
20
22
|
.strict()
|
|
21
23
|
|
|
@@ -23,6 +25,7 @@ export const claudeProviderOptionsSchema = z
|
|
|
23
25
|
.object({
|
|
24
26
|
effort: claudeEffortSchema.optional(),
|
|
25
27
|
model: modelSchema.optional(),
|
|
28
|
+
timeout: timeoutSchema.optional(),
|
|
26
29
|
})
|
|
27
30
|
.strict()
|
|
28
31
|
|
|
@@ -43,3 +46,17 @@ export function providerOptionsEqual(
|
|
|
43
46
|
left.effort === right.effort
|
|
44
47
|
)
|
|
45
48
|
}
|
|
49
|
+
|
|
50
|
+
export function providerOptionsCacheKey(options: {
|
|
51
|
+
effort?: string | undefined
|
|
52
|
+
model?: string | undefined
|
|
53
|
+
provider: 'claude' | 'codex'
|
|
54
|
+
timeout?: number | undefined
|
|
55
|
+
}) {
|
|
56
|
+
return [
|
|
57
|
+
options.provider,
|
|
58
|
+
options.model ?? '',
|
|
59
|
+
options.effort ?? '',
|
|
60
|
+
String(options.timeout ?? ''),
|
|
61
|
+
].join(':')
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class AgentTimeoutError extends Error {
|
|
2
|
+
public constructor(
|
|
3
|
+
public readonly provider: string,
|
|
4
|
+
public readonly timeout: number,
|
|
5
|
+
options?: ErrorOptions,
|
|
6
|
+
) {
|
|
7
|
+
super(`${provider} agent timed out after ${timeout}ms`, options)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function withAbortTimeout<T>(
|
|
12
|
+
provider: string,
|
|
13
|
+
timeout: number | undefined,
|
|
14
|
+
run: (controller: AbortController | undefined) => Promise<T>,
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
if (!timeout) {
|
|
17
|
+
return run(undefined)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const controller = new AbortController()
|
|
21
|
+
const state = { timedOut: false }
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
state.timedOut = true
|
|
24
|
+
controller.abort()
|
|
25
|
+
}, timeout)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await run(controller)
|
|
29
|
+
if (state.timedOut) {
|
|
30
|
+
throw new AgentTimeoutError(provider, timeout)
|
|
31
|
+
}
|
|
32
|
+
return result
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (state.timedOut) {
|
|
35
|
+
throw new AgentTimeoutError(provider, timeout, { cause: error })
|
|
36
|
+
}
|
|
37
|
+
throw error
|
|
38
|
+
} finally {
|
|
39
|
+
clearTimeout(timer)
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/batch/config.ts
CHANGED
|
@@ -97,6 +97,7 @@ export async function loadBatchConfig(
|
|
|
97
97
|
provider: 'claude',
|
|
98
98
|
...(parsedConfig.model ? { model: parsedConfig.model } : {}),
|
|
99
99
|
...(parsedConfig.effort ? { effort: parsedConfig.effort } : {}),
|
|
100
|
+
...(parsedConfig.timeout ? { timeout: parsedConfig.timeout } : {}),
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -105,5 +106,6 @@ export async function loadBatchConfig(
|
|
|
105
106
|
provider: 'codex',
|
|
106
107
|
...(parsedConfig.model ? { model: parsedConfig.model } : {}),
|
|
107
108
|
...(parsedConfig.effort ? { effort: parsedConfig.effort } : {}),
|
|
109
|
+
...(parsedConfig.timeout ? { timeout: parsedConfig.timeout } : {}),
|
|
108
110
|
}
|
|
109
111
|
}
|
package/src/batch/provider.ts
CHANGED
|
@@ -70,6 +70,7 @@ export function createBatchStructuredOutputProvider(
|
|
|
70
70
|
new CodexAgentClient({
|
|
71
71
|
...(input.effort ? { effort: input.effort } : {}),
|
|
72
72
|
...(input.model ? { model: input.model } : {}),
|
|
73
|
+
...(input.timeout ? { timeout: input.timeout } : {}),
|
|
73
74
|
...(onEvent ? { onEvent } : {}),
|
|
74
75
|
workspaceRoot: input.workspaceRoot,
|
|
75
76
|
}),
|
|
@@ -81,6 +82,7 @@ export function createBatchStructuredOutputProvider(
|
|
|
81
82
|
new ClaudeAgentClient({
|
|
82
83
|
...(input.effort ? { effort: input.effort } : {}),
|
|
83
84
|
...(input.model ? { model: input.model } : {}),
|
|
85
|
+
...(input.timeout ? { timeout: input.timeout } : {}),
|
|
84
86
|
...(onEvent ? { onEvent } : {}),
|
|
85
87
|
workspaceRoot: input.workspaceRoot,
|
|
86
88
|
}),
|
package/src/commands/batch.ts
CHANGED
|
@@ -42,6 +42,38 @@ export interface RunBatchCommandResult {
|
|
|
42
42
|
resultsFilePath: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function writeBatchVerboseLine(verbose: boolean | undefined, line: string) {
|
|
46
|
+
if (!verbose) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
process.stderr.write(`[batch] ${line}\n`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readSessionProgress(detail: unknown) {
|
|
53
|
+
if (typeof detail !== 'object' || detail === null) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
const progress = (detail as { progress?: unknown }).progress
|
|
57
|
+
if (typeof progress !== 'object' || progress === null) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const blocked =
|
|
62
|
+
typeof (progress as { blocked?: unknown }).blocked === 'number'
|
|
63
|
+
? (progress as { blocked: number }).blocked
|
|
64
|
+
: 0
|
|
65
|
+
const completed =
|
|
66
|
+
typeof (progress as { completed?: unknown }).completed === 'number'
|
|
67
|
+
? (progress as { completed: number }).completed
|
|
68
|
+
: 0
|
|
69
|
+
const suspended =
|
|
70
|
+
typeof (progress as { suspended?: unknown }).suspended === 'number'
|
|
71
|
+
? (progress as { suspended: number }).suspended
|
|
72
|
+
: 0
|
|
73
|
+
|
|
74
|
+
return { blocked, completed, suspended }
|
|
75
|
+
}
|
|
76
|
+
|
|
45
77
|
async function readJsonFileIfExists(filePath: string) {
|
|
46
78
|
const exists = await fsExtra.pathExists(filePath)
|
|
47
79
|
if (!exists) {
|
|
@@ -69,6 +101,7 @@ function createProvider(
|
|
|
69
101
|
provider: 'codex',
|
|
70
102
|
...(config.effort ? { effort: config.effort } : {}),
|
|
71
103
|
...(config.model ? { model: config.model } : {}),
|
|
104
|
+
...(config.timeout ? { timeout: config.timeout } : {}),
|
|
72
105
|
...(verbose === undefined ? {} : { verbose }),
|
|
73
106
|
workspaceRoot: config.configDir,
|
|
74
107
|
})
|
|
@@ -78,6 +111,7 @@ function createProvider(
|
|
|
78
111
|
provider: 'claude',
|
|
79
112
|
...(config.effort ? { effort: config.effort } : {}),
|
|
80
113
|
...(config.model ? { model: config.model } : {}),
|
|
114
|
+
...(config.timeout ? { timeout: config.timeout } : {}),
|
|
81
115
|
...(verbose === undefined ? {} : { verbose }),
|
|
82
116
|
workspaceRoot: config.configDir,
|
|
83
117
|
})
|
|
@@ -138,6 +172,10 @@ export async function runBatchCommand(
|
|
|
138
172
|
})
|
|
139
173
|
|
|
140
174
|
const processedFiles: string[] = []
|
|
175
|
+
const totalFiles = discoveredFiles.length
|
|
176
|
+
let blockedCount = 0
|
|
177
|
+
let completedCount = 0
|
|
178
|
+
let suspendedCount = 0
|
|
141
179
|
|
|
142
180
|
for await (const event of runSession({
|
|
143
181
|
config: {},
|
|
@@ -153,8 +191,70 @@ export async function runBatchCommand(
|
|
|
153
191
|
}),
|
|
154
192
|
},
|
|
155
193
|
})) {
|
|
194
|
+
if (event.type === SessionEventType.SessionStarted) {
|
|
195
|
+
const progress = readSessionProgress(event.detail)
|
|
196
|
+
if (progress) {
|
|
197
|
+
blockedCount = progress.blocked
|
|
198
|
+
completedCount = progress.completed
|
|
199
|
+
suspendedCount = progress.suspended
|
|
200
|
+
}
|
|
201
|
+
writeBatchVerboseLine(
|
|
202
|
+
input.verbose,
|
|
203
|
+
`resume total=${totalFiles} completed=${completedCount} blocked=${blockedCount} suspended=${suspendedCount}`,
|
|
204
|
+
)
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (event.type === SessionEventType.SubjectStarted) {
|
|
209
|
+
writeBatchVerboseLine(
|
|
210
|
+
input.verbose,
|
|
211
|
+
`start completed=${completedCount}/${totalFiles} file=${event.subjectId}`,
|
|
212
|
+
)
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (event.type === SessionEventType.SubjectResumed) {
|
|
217
|
+
suspendedCount = Math.max(0, suspendedCount - 1)
|
|
218
|
+
writeBatchVerboseLine(
|
|
219
|
+
input.verbose,
|
|
220
|
+
`resume-file completed=${completedCount}/${totalFiles} file=${event.subjectId}`,
|
|
221
|
+
)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
156
225
|
if (event.type === SessionEventType.SubjectDone) {
|
|
226
|
+
completedCount += 1
|
|
157
227
|
processedFiles.push(event.subjectId)
|
|
228
|
+
writeBatchVerboseLine(
|
|
229
|
+
input.verbose,
|
|
230
|
+
`done completed=${completedCount}/${totalFiles} file=${event.subjectId}`,
|
|
231
|
+
)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (event.type === SessionEventType.SubjectBlocked) {
|
|
236
|
+
blockedCount += 1
|
|
237
|
+
writeBatchVerboseLine(
|
|
238
|
+
input.verbose,
|
|
239
|
+
`blocked completed=${completedCount}/${totalFiles} file=${event.subjectId}`,
|
|
240
|
+
)
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (event.type === SessionEventType.SubjectSuspended) {
|
|
245
|
+
suspendedCount += 1
|
|
246
|
+
writeBatchVerboseLine(
|
|
247
|
+
input.verbose,
|
|
248
|
+
`suspended completed=${completedCount}/${totalFiles} file=${event.subjectId}`,
|
|
249
|
+
)
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (event.type === SessionEventType.SessionDone) {
|
|
254
|
+
writeBatchVerboseLine(
|
|
255
|
+
input.verbose,
|
|
256
|
+
`session-done total=${totalFiles} completed=${completedCount} blocked=${blockedCount} suspended=${suspendedCount}`,
|
|
257
|
+
)
|
|
158
258
|
}
|
|
159
259
|
}
|
|
160
260
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createClaudeEventHandler,
|
|
5
5
|
createCodexEventHandler,
|
|
6
6
|
} from '../agents/event-log'
|
|
7
|
+
import { providerOptionsCacheKey } from '../agents/provider-options'
|
|
7
8
|
import { createCodexRemoteReviewerProvider } from '../workflow/remote-reviewer'
|
|
8
9
|
|
|
9
10
|
import type {
|
|
@@ -26,12 +27,10 @@ export function createProviderResolver(
|
|
|
26
27
|
context: WorkspaceContext,
|
|
27
28
|
verbose: boolean | undefined,
|
|
28
29
|
): ProviderResolver {
|
|
29
|
-
const cache = new Map<
|
|
30
|
-
WorkflowProvider,
|
|
31
|
-
ImplementerProvider & ReviewerProvider
|
|
32
|
-
>()
|
|
30
|
+
const cache = new Map<string, ImplementerProvider & ReviewerProvider>()
|
|
33
31
|
return (role: WorkflowRoleConfig) => {
|
|
34
|
-
const
|
|
32
|
+
const cacheKey = providerOptionsCacheKey(role)
|
|
33
|
+
const cached = cache.get(cacheKey)
|
|
35
34
|
if (cached) {
|
|
36
35
|
return cached
|
|
37
36
|
}
|
|
@@ -41,6 +40,7 @@ export function createProviderResolver(
|
|
|
41
40
|
provider = createClaudeProvider({
|
|
42
41
|
...(role.effort ? { effort: role.effort } : {}),
|
|
43
42
|
...(role.model ? { model: role.model } : {}),
|
|
43
|
+
...(role.timeout ? { timeout: role.timeout } : {}),
|
|
44
44
|
workspaceRoot: context.workspaceRoot,
|
|
45
45
|
...(onEvent ? { onEvent } : {}),
|
|
46
46
|
})
|
|
@@ -49,11 +49,12 @@ export function createProviderResolver(
|
|
|
49
49
|
provider = createCodexProvider({
|
|
50
50
|
...(role.effort ? { effort: role.effort } : {}),
|
|
51
51
|
...(role.model ? { model: role.model } : {}),
|
|
52
|
+
...(role.timeout ? { timeout: role.timeout } : {}),
|
|
52
53
|
workspaceRoot: context.workspaceRoot,
|
|
53
54
|
...(onEvent ? { onEvent } : {}),
|
|
54
55
|
})
|
|
55
56
|
}
|
|
56
|
-
cache.set(
|
|
57
|
+
cache.set(cacheKey, provider)
|
|
57
58
|
return provider
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createClaudeEventHandler,
|
|
11
11
|
createCodexEventHandler,
|
|
12
12
|
} from '../agents/event-log'
|
|
13
|
+
import { providerOptionsCacheKey } from '../agents/provider-options'
|
|
13
14
|
import { GitRuntime } from '../runtime/git'
|
|
14
15
|
import { GitHubRuntime } from '../runtime/github'
|
|
15
16
|
import { createRuntimePaths } from '../runtime/path-layout'
|
|
@@ -54,7 +55,7 @@ export function createRuntimePorts(
|
|
|
54
55
|
const agentCache = new Map<string, AgentPort>()
|
|
55
56
|
|
|
56
57
|
function resolveAgent(role: AgentRoleConfig): AgentPort {
|
|
57
|
-
const key =
|
|
58
|
+
const key = providerOptionsCacheKey(role)
|
|
58
59
|
const cached = agentCache.get(key)
|
|
59
60
|
if (cached) {
|
|
60
61
|
return cached
|
|
@@ -72,6 +73,7 @@ export function createRuntimePorts(
|
|
|
72
73
|
}
|
|
73
74
|
: {}),
|
|
74
75
|
...(role.model ? { model: role.model } : {}),
|
|
76
|
+
...(role.timeout ? { timeout: role.timeout } : {}),
|
|
75
77
|
workspaceRoot: context.workspaceRoot,
|
|
76
78
|
...(onEvent ? { onEvent } : {}),
|
|
77
79
|
})
|
|
@@ -86,6 +88,7 @@ export function createRuntimePorts(
|
|
|
86
88
|
}
|
|
87
89
|
: {}),
|
|
88
90
|
...(role.model ? { model: role.model } : {}),
|
|
91
|
+
...(role.timeout ? { timeout: role.timeout } : {}),
|
|
89
92
|
workspaceRoot: context.workspaceRoot,
|
|
90
93
|
...(onEvent ? { onEvent } : {}),
|
|
91
94
|
})
|