task-while 0.0.2 → 0.0.3
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 +32 -34
- package/package.json +2 -2
- package/src/adapters/fs/harness-store.ts +84 -0
- package/src/agents/claude.ts +159 -9
- package/src/agents/codex.ts +68 -4
- package/src/agents/event-log.ts +160 -15
- package/src/batch/discovery.ts +1 -1
- package/src/commands/batch.ts +63 -164
- package/src/commands/run-branch-helpers.ts +81 -0
- package/src/commands/run-providers.ts +77 -0
- package/src/commands/run.ts +121 -177
- package/src/core/create-runtime-ports.ts +118 -0
- package/src/core/runtime.ts +15 -36
- package/src/harness/in-memory-store.ts +45 -0
- package/src/harness/kernel.ts +226 -0
- package/src/harness/state.ts +47 -0
- package/src/harness/store.ts +26 -0
- package/src/harness/workflow-builders.ts +87 -0
- package/src/harness/workflow-program.ts +86 -0
- package/src/ports/agent.ts +17 -0
- package/src/ports/code-host.ts +23 -0
- package/src/programs/batch.ts +139 -0
- package/src/programs/run-direct.ts +209 -0
- package/src/programs/run-pr-transitions.ts +81 -0
- package/src/programs/run-pr.ts +290 -0
- package/src/programs/shared-steps.ts +252 -0
- package/src/schedulers/scheduler.ts +208 -0
- package/src/session/session.ts +127 -0
- package/src/workflow/config.ts +15 -0
- package/src/core/engine-helpers.ts +0 -114
- package/src/core/engine-outcomes.ts +0 -166
- package/src/core/engine.ts +0 -223
- package/src/core/orchestrator-helpers.ts +0 -52
- package/src/core/orchestrator-integrate-resume.ts +0 -149
- package/src/core/orchestrator-review-resume.ts +0 -228
- package/src/core/orchestrator-task-attempt.ts +0 -257
- package/src/core/orchestrator.ts +0 -99
- package/src/runtime/fs-runtime.ts +0 -209
- package/src/workflow/direct-preset.ts +0 -44
- package/src/workflow/preset.ts +0 -86
- package/src/workflow/pull-request-preset.ts +0 -312
package/src/agents/event-log.ts
CHANGED
|
@@ -1,34 +1,179 @@
|
|
|
1
1
|
import type { ClaudeAgentEvent, ClaudeAgentEventHandler } from './claude'
|
|
2
2
|
import type { CodexThreadEvent, CodexThreadEventHandler } from './codex'
|
|
3
3
|
|
|
4
|
+
function formatInline(value: string) {
|
|
5
|
+
return value.trim().replaceAll('\n', String.raw`\n`)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatJson(value: unknown) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify(value)
|
|
11
|
+
} catch {
|
|
12
|
+
return String(value)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
function writeCodexEvent(event: CodexThreadEvent) {
|
|
5
|
-
|
|
17
|
+
if (
|
|
6
18
|
event.type === 'item.completed' ||
|
|
7
19
|
event.type === 'item.started' ||
|
|
8
20
|
event.type === 'item.updated'
|
|
9
|
-
? event.item.type
|
|
10
|
-
: null
|
|
11
|
-
process.stderr.write(
|
|
12
|
-
`[codex] ${event.type}${itemType ? ` ${itemType}` : ''}\n`,
|
|
13
|
-
)
|
|
14
|
-
if (
|
|
15
|
-
event.type === 'item.completed' &&
|
|
16
|
-
event.item.type === 'agent_message' &&
|
|
17
|
-
event.item.text?.trim()
|
|
18
21
|
) {
|
|
19
|
-
|
|
22
|
+
const item = event.item
|
|
23
|
+
|
|
24
|
+
if (item.type === 'reasoning') {
|
|
25
|
+
const text = formatInline(item.text)
|
|
26
|
+
if (text) {
|
|
27
|
+
process.stderr.write(`[codex] thinking ${text}\n`)
|
|
28
|
+
}
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (item.type === 'command_execution') {
|
|
33
|
+
if (event.type === 'item.started') {
|
|
34
|
+
process.stderr.write(`[codex] exec ${formatInline(item.command)}\n`)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if (event.type === 'item.completed') {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
`[codex] exec ${item.status} exit=${item.exit_code ?? 'unknown'} ${formatInline(item.command)}\n`,
|
|
40
|
+
)
|
|
41
|
+
const output = formatInline(item.aggregated_output)
|
|
42
|
+
if (output) {
|
|
43
|
+
process.stderr.write(`[codex] output ${output}\n`)
|
|
44
|
+
}
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (item.type === 'mcp_tool_call') {
|
|
50
|
+
const target = `${item.server}.${item.tool}`
|
|
51
|
+
if (event.type === 'item.started') {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`[codex] tool ${target} ${formatJson(item.arguments)}\n`,
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (event.type === 'item.completed') {
|
|
58
|
+
const detail =
|
|
59
|
+
item.status === 'failed'
|
|
60
|
+
? ` error=${item.error?.message ?? 'unknown'}`
|
|
61
|
+
: ''
|
|
62
|
+
process.stderr.write(`[codex] tool ${item.status} ${target}${detail}\n`)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (item.type === 'file_change' && event.type === 'item.completed') {
|
|
68
|
+
const files = item.changes.map((change) => change.path).join(', ')
|
|
69
|
+
process.stderr.write(`[codex] files ${item.status} ${files}\n`)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (item.type === 'web_search') {
|
|
74
|
+
process.stderr.write(`[codex] search ${formatInline(item.query)}\n`)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (item.type === 'todo_list') {
|
|
79
|
+
for (const todo of item.items) {
|
|
80
|
+
process.stderr.write(
|
|
81
|
+
`[codex] todo ${todo.completed ? '[x]' : '[ ]'} ${formatInline(todo.text)}\n`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (item.type === 'error') {
|
|
88
|
+
process.stderr.write(`[codex] error ${formatInline(item.message)}\n`)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (item.type === 'agent_message' && event.type === 'item.completed') {
|
|
93
|
+
const text = formatInline(item.text)
|
|
94
|
+
if (text) {
|
|
95
|
+
process.stderr.write(`[codex] message ${text}\n`)
|
|
96
|
+
}
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (event.type === 'turn.completed') {
|
|
102
|
+
process.stderr.write(
|
|
103
|
+
`[codex] result tokens in=${event.usage.input_tokens} out=${event.usage.output_tokens} cached=${event.usage.cached_input_tokens}\n`,
|
|
104
|
+
)
|
|
105
|
+
return
|
|
20
106
|
}
|
|
107
|
+
|
|
21
108
|
if (event.type === 'error') {
|
|
22
|
-
process.stderr.write(`[codex] error ${event.message}\n`)
|
|
109
|
+
process.stderr.write(`[codex] error ${formatInline(event.message)}\n`)
|
|
110
|
+
return
|
|
23
111
|
}
|
|
112
|
+
|
|
24
113
|
if (event.type === 'turn.failed') {
|
|
25
|
-
process.stderr.write(`[codex] error ${event.error.message}\n`)
|
|
114
|
+
process.stderr.write(`[codex] error ${formatInline(event.error.message)}\n`)
|
|
26
115
|
}
|
|
27
116
|
}
|
|
28
117
|
|
|
29
118
|
function writeClaudeEvent(event: ClaudeAgentEvent) {
|
|
30
|
-
|
|
31
|
-
|
|
119
|
+
if (event.type === 'system.init') {
|
|
120
|
+
const tools = event.tools.length !== 0 ? event.tools.join(',') : '-'
|
|
121
|
+
const skills = event.skills.length !== 0 ? event.skills.join(',') : '-'
|
|
122
|
+
const mcp =
|
|
123
|
+
event.mcpServers.length !== 0
|
|
124
|
+
? event.mcpServers
|
|
125
|
+
.map((server) => `${server.name}:${server.status}`)
|
|
126
|
+
.join(',')
|
|
127
|
+
: '-'
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`[claude] init model=${event.model} permission=${event.permissionMode} tools=${tools} skills=${skills} mcp=${mcp}\n`,
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (event.type === 'task.started') {
|
|
135
|
+
process.stderr.write(`[claude] task ${formatInline(event.description)}\n`)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (event.type === 'tool.progress') {
|
|
140
|
+
process.stderr.write(
|
|
141
|
+
`[claude] tool ${event.toolName} ${event.elapsedTimeSeconds}s\n`,
|
|
142
|
+
)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (event.type === 'tool.summary') {
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
`[claude] tool-summary ${formatInline(event.summary)}\n`,
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (event.type === 'task.progress') {
|
|
154
|
+
const detail = event.summary ?? event.description
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
`[claude] progress ${formatInline(event.lastToolName ?? '-')} ${formatInline(detail)}\n`,
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (event.type === 'text') {
|
|
162
|
+
const text = formatInline(event.delta)
|
|
163
|
+
if (text) {
|
|
164
|
+
process.stderr.write(`[claude] text ${text}\n`)
|
|
165
|
+
}
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (event.type === 'result') {
|
|
170
|
+
process.stderr.write(
|
|
171
|
+
`[claude] result ${event.subtype} turns=${event.numTurns} duration=${event.durationMs}ms\n`,
|
|
172
|
+
)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
process.stderr.write(`[claude] error ${formatInline(event.message)}\n`)
|
|
32
177
|
}
|
|
33
178
|
|
|
34
179
|
export function createCodexEventHandler(
|
package/src/batch/discovery.ts
CHANGED
package/src/commands/batch.ts
CHANGED
|
@@ -5,23 +5,21 @@ import Ajv from 'ajv'
|
|
|
5
5
|
import * as fsExtra from 'fs-extra'
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
|
|
8
|
+
import { createFsHarnessStore } from '../adapters/fs/harness-store'
|
|
8
9
|
import { loadBatchConfig, type BatchConfig } from '../batch/config'
|
|
9
10
|
import { discoverBatchFiles } from '../batch/discovery'
|
|
10
11
|
import {
|
|
11
12
|
createBatchStructuredOutputProvider,
|
|
12
13
|
type BatchStructuredOutputProvider,
|
|
13
14
|
} from '../batch/provider'
|
|
14
|
-
import {
|
|
15
|
+
import { runKernel } from '../harness/kernel'
|
|
16
|
+
import { createBatchProgram } from '../programs/batch'
|
|
17
|
+
import { createRuntimePaths } from '../runtime/path-layout'
|
|
18
|
+
import { createBatchRetryScheduler } from '../schedulers/scheduler'
|
|
19
|
+
import { parseWithSchema } from '../schema/shared'
|
|
20
|
+
import { runSession, SessionEventType } from '../session/session'
|
|
15
21
|
import { writeJsonAtomic } from '../utils/fs'
|
|
16
22
|
|
|
17
|
-
const batchStateSchema = z
|
|
18
|
-
.object({
|
|
19
|
-
failed: uniqueStringArray('failed'),
|
|
20
|
-
inProgress: uniqueStringArray('inProgress'),
|
|
21
|
-
pending: uniqueStringArray('pending'),
|
|
22
|
-
})
|
|
23
|
-
.strict()
|
|
24
|
-
|
|
25
23
|
const batchResultsSchema = z.custom<Record<string, unknown>>(
|
|
26
24
|
(value) =>
|
|
27
25
|
typeof value === 'object' && value !== null && !Array.isArray(value),
|
|
@@ -30,12 +28,6 @@ const batchResultsSchema = z.custom<Record<string, unknown>>(
|
|
|
30
28
|
},
|
|
31
29
|
)
|
|
32
30
|
|
|
33
|
-
export interface BatchState {
|
|
34
|
-
failed: string[]
|
|
35
|
-
inProgress: string[]
|
|
36
|
-
pending: string[]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
31
|
export interface RunBatchCommandInput {
|
|
40
32
|
configPath: string
|
|
41
33
|
cwd?: string
|
|
@@ -47,19 +39,7 @@ export interface RunBatchCommandResult {
|
|
|
47
39
|
failedFiles: string[]
|
|
48
40
|
processedFiles: string[]
|
|
49
41
|
results: Record<string, unknown>
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function createEmptyState(): BatchState {
|
|
54
|
-
return {
|
|
55
|
-
failed: [],
|
|
56
|
-
inProgress: [],
|
|
57
|
-
pending: [],
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function unique(items: string[]) {
|
|
62
|
-
return [...new Set(items)]
|
|
42
|
+
resultsFilePath: string
|
|
63
43
|
}
|
|
64
44
|
|
|
65
45
|
async function readJsonFileIfExists(filePath: string) {
|
|
@@ -72,14 +52,6 @@ async function readJsonFileIfExists(filePath: string) {
|
|
|
72
52
|
return value
|
|
73
53
|
}
|
|
74
54
|
|
|
75
|
-
async function loadBatchState(filePath: string) {
|
|
76
|
-
const value = await readJsonFileIfExists(filePath)
|
|
77
|
-
if (value === null) {
|
|
78
|
-
return createEmptyState()
|
|
79
|
-
}
|
|
80
|
-
return parseWithSchema(batchStateSchema, value)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
55
|
async function loadBatchResults(filePath: string) {
|
|
84
56
|
const value = await readJsonFileIfExists(filePath)
|
|
85
57
|
if (value === null) {
|
|
@@ -88,76 +60,6 @@ async function loadBatchResults(filePath: string) {
|
|
|
88
60
|
return parseWithSchema(batchResultsSchema, value)
|
|
89
61
|
}
|
|
90
62
|
|
|
91
|
-
function mergeBatchState(input: {
|
|
92
|
-
discoveredFiles: string[]
|
|
93
|
-
results: Record<string, unknown>
|
|
94
|
-
state: BatchState
|
|
95
|
-
}): BatchState {
|
|
96
|
-
const discovered = new Set(input.discoveredFiles)
|
|
97
|
-
const completed = new Set(Object.keys(input.results))
|
|
98
|
-
const failed = unique(input.state.failed).filter(
|
|
99
|
-
(filePath) => discovered.has(filePath) && !completed.has(filePath),
|
|
100
|
-
)
|
|
101
|
-
const failedSet = new Set(failed)
|
|
102
|
-
const pending = unique([
|
|
103
|
-
...input.state.inProgress,
|
|
104
|
-
...input.state.pending,
|
|
105
|
-
]).filter(
|
|
106
|
-
(filePath) =>
|
|
107
|
-
discovered.has(filePath) &&
|
|
108
|
-
!completed.has(filePath) &&
|
|
109
|
-
!failedSet.has(filePath),
|
|
110
|
-
)
|
|
111
|
-
const pendingSet = new Set(pending)
|
|
112
|
-
|
|
113
|
-
for (const filePath of input.discoveredFiles) {
|
|
114
|
-
if (
|
|
115
|
-
completed.has(filePath) ||
|
|
116
|
-
failedSet.has(filePath) ||
|
|
117
|
-
pendingSet.has(filePath)
|
|
118
|
-
) {
|
|
119
|
-
continue
|
|
120
|
-
}
|
|
121
|
-
pending.push(filePath)
|
|
122
|
-
pendingSet.add(filePath)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
failed,
|
|
127
|
-
inProgress: [],
|
|
128
|
-
pending,
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function removeFile(items: string[], filePath: string) {
|
|
133
|
-
return items.filter((item) => item !== filePath)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function writeBatchFailure(filePath: string, error: unknown) {
|
|
137
|
-
process.stderr.write(
|
|
138
|
-
`[batch] failed ${filePath}: ${
|
|
139
|
-
error instanceof Error ? error.message : String(error)
|
|
140
|
-
}\n`,
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function recycleFailedFiles(
|
|
145
|
-
statePath: string,
|
|
146
|
-
state: BatchState,
|
|
147
|
-
): Promise<BatchState> {
|
|
148
|
-
if (state.pending.length !== 0 || state.failed.length === 0) {
|
|
149
|
-
return state
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const nextState: BatchState = {
|
|
153
|
-
failed: [],
|
|
154
|
-
inProgress: [],
|
|
155
|
-
pending: [...state.failed],
|
|
156
|
-
}
|
|
157
|
-
await writeJsonAtomic(statePath, nextState)
|
|
158
|
-
return nextState
|
|
159
|
-
}
|
|
160
|
-
|
|
161
63
|
function createProvider(
|
|
162
64
|
config: BatchConfig,
|
|
163
65
|
verbose: boolean | undefined,
|
|
@@ -181,6 +83,17 @@ function createProvider(
|
|
|
181
83
|
})
|
|
182
84
|
}
|
|
183
85
|
|
|
86
|
+
function createOutputValidator(schema: Record<string, unknown>) {
|
|
87
|
+
const ajv = new Ajv({ strict: false })
|
|
88
|
+
const validate = ajv.compile(schema)
|
|
89
|
+
return (value: unknown) => {
|
|
90
|
+
if (validate(value)) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
throw new Error(ajv.errorsText(validate.errors))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
184
97
|
export async function runBatchCommand(
|
|
185
98
|
input: RunBatchCommandInput,
|
|
186
99
|
): Promise<RunBatchCommandResult> {
|
|
@@ -190,82 +103,68 @@ export async function runBatchCommand(
|
|
|
190
103
|
cwd,
|
|
191
104
|
})
|
|
192
105
|
|
|
193
|
-
const statePath = path.join(config.configDir, 'state.json')
|
|
194
106
|
const resultsPath = path.join(config.configDir, 'results.json')
|
|
195
|
-
const excludedFiles = new Set([config.configPath,
|
|
107
|
+
const excludedFiles = new Set([config.configPath, resultsPath])
|
|
196
108
|
const discoveredFiles = await discoverBatchFiles({
|
|
197
109
|
baseDir: config.configDir,
|
|
198
110
|
excludedFiles,
|
|
199
111
|
patterns: config.glob,
|
|
200
112
|
})
|
|
201
113
|
const results = await loadBatchResults(resultsPath)
|
|
202
|
-
|
|
203
|
-
|
|
114
|
+
await writeJsonAtomic(resultsPath, results)
|
|
115
|
+
|
|
116
|
+
const provider = createProvider(config, input.verbose)
|
|
117
|
+
const validateOutput = createOutputValidator(config.schema)
|
|
118
|
+
const harnessDir = createRuntimePaths(config.configDir).runtimeDir
|
|
119
|
+
const store = createFsHarnessStore(harnessDir)
|
|
120
|
+
const protocol = 'batch'
|
|
121
|
+
|
|
122
|
+
const program = createBatchProgram({
|
|
123
|
+
configDir: config.configDir,
|
|
124
|
+
maxRetries: 3,
|
|
125
|
+
outputSchema: config.schema,
|
|
126
|
+
prompt: config.prompt,
|
|
127
|
+
provider,
|
|
204
128
|
results,
|
|
205
|
-
|
|
129
|
+
resultsPath,
|
|
130
|
+
validateOutput,
|
|
206
131
|
})
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
132
|
+
|
|
133
|
+
const scheduler = createBatchRetryScheduler({
|
|
134
|
+
files: discoveredFiles,
|
|
135
|
+
protocol,
|
|
136
|
+
results,
|
|
137
|
+
store,
|
|
212
138
|
})
|
|
213
|
-
const validateOutput = ajv.compile(config.schema)
|
|
214
|
-
const processedFiles: string[] = []
|
|
215
|
-
let provider: BatchStructuredOutputProvider | null = null
|
|
216
139
|
|
|
217
|
-
|
|
218
|
-
if (state.pending.length === 0) {
|
|
219
|
-
state = await recycleFailedFiles(statePath, state)
|
|
220
|
-
continue
|
|
221
|
-
}
|
|
222
|
-
const filePath = state.pending[0]!
|
|
223
|
-
state = {
|
|
224
|
-
...state,
|
|
225
|
-
inProgress: unique([...state.inProgress, filePath]),
|
|
226
|
-
pending: state.pending.slice(1),
|
|
227
|
-
}
|
|
228
|
-
await writeJsonAtomic(statePath, state)
|
|
140
|
+
const processedFiles: string[] = []
|
|
229
141
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
...state,
|
|
247
|
-
inProgress: removeFile(state.inProgress, filePath),
|
|
248
|
-
}
|
|
249
|
-
await writeJsonAtomic(statePath, state)
|
|
250
|
-
processedFiles.push(filePath)
|
|
251
|
-
} catch (error) {
|
|
252
|
-
if (input.verbose) {
|
|
253
|
-
writeBatchFailure(filePath, error)
|
|
254
|
-
}
|
|
255
|
-
state = {
|
|
256
|
-
failed: unique([...state.failed, filePath]),
|
|
257
|
-
inProgress: removeFile(state.inProgress, filePath),
|
|
258
|
-
pending: state.pending,
|
|
259
|
-
}
|
|
260
|
-
await writeJsonAtomic(statePath, state)
|
|
142
|
+
for await (const event of runSession({
|
|
143
|
+
config: {},
|
|
144
|
+
scheduler,
|
|
145
|
+
kernel: {
|
|
146
|
+
run: (subjectId) =>
|
|
147
|
+
runKernel({
|
|
148
|
+
config: { prompt: config.prompt, schema: config.schema },
|
|
149
|
+
program,
|
|
150
|
+
protocol,
|
|
151
|
+
store,
|
|
152
|
+
subjectId,
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
})) {
|
|
156
|
+
if (event.type === SessionEventType.SubjectDone) {
|
|
157
|
+
processedFiles.push(event.subjectId)
|
|
261
158
|
}
|
|
262
159
|
}
|
|
263
160
|
|
|
161
|
+
const sets = await scheduler.rebuild()
|
|
162
|
+
|
|
264
163
|
return {
|
|
265
164
|
config,
|
|
266
|
-
failedFiles:
|
|
165
|
+
failedFiles: [...sets.blocked],
|
|
267
166
|
processedFiles,
|
|
268
167
|
results,
|
|
269
|
-
|
|
168
|
+
resultsFilePath: resultsPath,
|
|
270
169
|
}
|
|
271
170
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { GitPort } from '../core/runtime'
|
|
2
|
+
import type { CodeHostPort } from '../ports/code-host'
|
|
3
|
+
import type { TaskSourceSession } from '../task-sources/types'
|
|
4
|
+
|
|
5
|
+
export const sleep = (ms: number) =>
|
|
6
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
7
|
+
|
|
8
|
+
export function toTaskBranchName(commitSubject: string) {
|
|
9
|
+
const slug = commitSubject
|
|
10
|
+
.replace(/^Task\s+/i, '')
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '')
|
|
14
|
+
return `task/${slug}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function ensureTaskBranch(
|
|
18
|
+
git: GitPort,
|
|
19
|
+
branchName: string,
|
|
20
|
+
restoreFromRemote: boolean,
|
|
21
|
+
) {
|
|
22
|
+
const currentBranch = await git.getCurrentBranch()
|
|
23
|
+
if (currentBranch === branchName) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
await git.checkoutBranch(branchName)
|
|
28
|
+
} catch {
|
|
29
|
+
if (restoreFromRemote) {
|
|
30
|
+
await git.checkoutRemoteBranch(branchName)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
await git.checkoutBranch(branchName, {
|
|
34
|
+
create: true,
|
|
35
|
+
startPoint: 'main',
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runPrCheckpoint(
|
|
41
|
+
ports: { codeHost: CodeHostPort; git: GitPort },
|
|
42
|
+
taskSource: TaskSourceSession,
|
|
43
|
+
input: { iteration: number; subjectId: string },
|
|
44
|
+
): Promise<{ checkpointStartedAt: string; prNumber: number }> {
|
|
45
|
+
const commitSubject = taskSource.buildCommitSubject(input.subjectId)
|
|
46
|
+
const branchName = toTaskBranchName(commitSubject)
|
|
47
|
+
const existingPr = await ports.codeHost.findOpenPullRequestByHeadBranch({
|
|
48
|
+
headBranch: branchName,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await ensureTaskBranch(ports.git, branchName, existingPr !== null)
|
|
52
|
+
|
|
53
|
+
const checkpointMessage = `checkpoint: ${commitSubject} (attempt ${input.iteration})`
|
|
54
|
+
const headSubject = await ports.git.getHeadSubject()
|
|
55
|
+
if (headSubject !== checkpointMessage) {
|
|
56
|
+
await ports.git.commitTask({ message: checkpointMessage })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await ports.git.pushBranch(branchName)
|
|
60
|
+
|
|
61
|
+
let pullRequest = existingPr
|
|
62
|
+
if (!pullRequest) {
|
|
63
|
+
pullRequest = await ports.codeHost.createPullRequest({
|
|
64
|
+
baseBranch: 'main',
|
|
65
|
+
body: `Task: ${commitSubject}\nManaged by task-while.`,
|
|
66
|
+
headBranch: branchName,
|
|
67
|
+
title: commitSubject,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const checkpointStartedAt = await ports.git.getHeadTimestamp()
|
|
72
|
+
return { checkpointStartedAt, prNumber: pullRequest.number }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function cleanupBranch(git: GitPort, branchName: string) {
|
|
76
|
+
try {
|
|
77
|
+
await git.checkoutBranch('main')
|
|
78
|
+
await git.pullFastForward('main')
|
|
79
|
+
await git.deleteLocalBranch(branchName)
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createClaudeProvider } from '../agents/claude'
|
|
2
|
+
import { createCodexProvider } from '../agents/codex'
|
|
3
|
+
import {
|
|
4
|
+
createClaudeEventHandler,
|
|
5
|
+
createCodexEventHandler,
|
|
6
|
+
} from '../agents/event-log'
|
|
7
|
+
import { createCodexRemoteReviewerProvider } from '../workflow/remote-reviewer'
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ImplementerProvider,
|
|
11
|
+
RemoteReviewerProvider,
|
|
12
|
+
ReviewerProvider,
|
|
13
|
+
} from '../agents/types'
|
|
14
|
+
import type { WorkspaceContext } from '../types'
|
|
15
|
+
import type { WorkflowProvider, WorkflowRoleConfig } from '../workflow/config'
|
|
16
|
+
|
|
17
|
+
export type ProviderResolver = (
|
|
18
|
+
role: WorkflowRoleConfig,
|
|
19
|
+
) => ImplementerProvider & ReviewerProvider
|
|
20
|
+
|
|
21
|
+
export type RemoteReviewerResolver = (
|
|
22
|
+
providerName: WorkflowProvider,
|
|
23
|
+
) => RemoteReviewerProvider
|
|
24
|
+
|
|
25
|
+
export function createProviderResolver(
|
|
26
|
+
context: WorkspaceContext,
|
|
27
|
+
verbose: boolean | undefined,
|
|
28
|
+
): ProviderResolver {
|
|
29
|
+
const cache = new Map<
|
|
30
|
+
WorkflowProvider,
|
|
31
|
+
ImplementerProvider & ReviewerProvider
|
|
32
|
+
>()
|
|
33
|
+
return (role: WorkflowRoleConfig) => {
|
|
34
|
+
const cached = cache.get(role.provider)
|
|
35
|
+
if (cached) {
|
|
36
|
+
return cached
|
|
37
|
+
}
|
|
38
|
+
let provider: ImplementerProvider & ReviewerProvider
|
|
39
|
+
if (role.provider === 'claude') {
|
|
40
|
+
const onEvent = createClaudeEventHandler(verbose)
|
|
41
|
+
provider = createClaudeProvider({
|
|
42
|
+
...(role.effort ? { effort: role.effort } : {}),
|
|
43
|
+
...(role.model ? { model: role.model } : {}),
|
|
44
|
+
workspaceRoot: context.workspaceRoot,
|
|
45
|
+
...(onEvent ? { onEvent } : {}),
|
|
46
|
+
})
|
|
47
|
+
} else {
|
|
48
|
+
const onEvent = createCodexEventHandler(verbose)
|
|
49
|
+
provider = createCodexProvider({
|
|
50
|
+
...(role.effort ? { effort: role.effort } : {}),
|
|
51
|
+
...(role.model ? { model: role.model } : {}),
|
|
52
|
+
workspaceRoot: context.workspaceRoot,
|
|
53
|
+
...(onEvent ? { onEvent } : {}),
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
cache.set(role.provider, provider)
|
|
57
|
+
return provider
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createRemoteReviewerResolver(): RemoteReviewerResolver {
|
|
62
|
+
const cache = new Map<WorkflowProvider, RemoteReviewerProvider>()
|
|
63
|
+
return (providerName: WorkflowProvider) => {
|
|
64
|
+
const cached = cache.get(providerName)
|
|
65
|
+
if (cached) {
|
|
66
|
+
return cached
|
|
67
|
+
}
|
|
68
|
+
if (providerName === 'claude') {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'claude remote reviewer is not implemented in pull-request mode',
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
const provider = createCodexRemoteReviewerProvider()
|
|
74
|
+
cache.set(providerName, provider)
|
|
75
|
+
return provider
|
|
76
|
+
}
|
|
77
|
+
}
|