task-while 0.0.1 → 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 +195 -0
- package/src/batch/discovery.ts +1 -1
- package/src/batch/provider.ts +9 -0
- package/src/commands/batch.ts +69 -165
- package/src/commands/run-branch-helpers.ts +81 -0
- package/src/commands/run-providers.ts +77 -0
- package/src/commands/run.ts +117 -225
- 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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { ClaudeAgentEvent, ClaudeAgentEventHandler } from './claude'
|
|
2
|
+
import type { CodexThreadEvent, CodexThreadEventHandler } from './codex'
|
|
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
|
+
|
|
16
|
+
function writeCodexEvent(event: CodexThreadEvent) {
|
|
17
|
+
if (
|
|
18
|
+
event.type === 'item.completed' ||
|
|
19
|
+
event.type === 'item.started' ||
|
|
20
|
+
event.type === 'item.updated'
|
|
21
|
+
) {
|
|
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
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (event.type === 'error') {
|
|
109
|
+
process.stderr.write(`[codex] error ${formatInline(event.message)}\n`)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (event.type === 'turn.failed') {
|
|
114
|
+
process.stderr.write(`[codex] error ${formatInline(event.error.message)}\n`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeClaudeEvent(event: ClaudeAgentEvent) {
|
|
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`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function createCodexEventHandler(
|
|
180
|
+
verbose: boolean | undefined,
|
|
181
|
+
): CodexThreadEventHandler | undefined {
|
|
182
|
+
if (!verbose) {
|
|
183
|
+
return undefined
|
|
184
|
+
}
|
|
185
|
+
return writeCodexEvent
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createClaudeEventHandler(
|
|
189
|
+
verbose: boolean | undefined,
|
|
190
|
+
): ClaudeAgentEventHandler | undefined {
|
|
191
|
+
if (!verbose) {
|
|
192
|
+
return undefined
|
|
193
|
+
}
|
|
194
|
+
return writeClaudeEvent
|
|
195
|
+
}
|
package/src/batch/discovery.ts
CHANGED
package/src/batch/provider.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ClaudeAgentClient } from '../agents/claude'
|
|
2
2
|
import { CodexAgentClient } from '../agents/codex'
|
|
3
|
+
import {
|
|
4
|
+
createClaudeEventHandler,
|
|
5
|
+
createCodexEventHandler,
|
|
6
|
+
} from '../agents/event-log'
|
|
3
7
|
|
|
4
8
|
import type { WorkflowRoleProviderOptions } from '../agents/provider-options'
|
|
5
9
|
|
|
@@ -17,6 +21,7 @@ export interface BatchStructuredOutputProvider {
|
|
|
17
21
|
|
|
18
22
|
export type CreateBatchStructuredOutputProviderInput =
|
|
19
23
|
WorkflowRoleProviderOptions & {
|
|
24
|
+
verbose?: boolean
|
|
20
25
|
workspaceRoot: string
|
|
21
26
|
}
|
|
22
27
|
|
|
@@ -60,19 +65,23 @@ export function createBatchStructuredOutputProvider(
|
|
|
60
65
|
input: CreateBatchStructuredOutputProviderInput,
|
|
61
66
|
): BatchStructuredOutputProvider {
|
|
62
67
|
if (input.provider === 'codex') {
|
|
68
|
+
const onEvent = createCodexEventHandler(input.verbose)
|
|
63
69
|
return new CodexBatchStructuredOutputProvider(
|
|
64
70
|
new CodexAgentClient({
|
|
65
71
|
...(input.effort ? { effort: input.effort } : {}),
|
|
66
72
|
...(input.model ? { model: input.model } : {}),
|
|
73
|
+
...(onEvent ? { onEvent } : {}),
|
|
67
74
|
workspaceRoot: input.workspaceRoot,
|
|
68
75
|
}),
|
|
69
76
|
)
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
const onEvent = createClaudeEventHandler(input.verbose)
|
|
72
80
|
return new ClaudeBatchStructuredOutputProvider(
|
|
73
81
|
new ClaudeAgentClient({
|
|
74
82
|
...(input.effort ? { effort: input.effort } : {}),
|
|
75
83
|
...(input.model ? { model: input.model } : {}),
|
|
84
|
+
...(onEvent ? { onEvent } : {}),
|
|
76
85
|
workspaceRoot: input.workspaceRoot,
|
|
77
86
|
}),
|
|
78
87
|
)
|
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,82 +60,16 @@ async function loadBatchResults(filePath: string) {
|
|
|
88
60
|
return parseWithSchema(batchResultsSchema, value)
|
|
89
61
|
}
|
|
90
62
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
function createProvider(config: BatchConfig): BatchStructuredOutputProvider {
|
|
63
|
+
function createProvider(
|
|
64
|
+
config: BatchConfig,
|
|
65
|
+
verbose: boolean | undefined,
|
|
66
|
+
): BatchStructuredOutputProvider {
|
|
162
67
|
if (config.provider === 'codex') {
|
|
163
68
|
return createBatchStructuredOutputProvider({
|
|
164
69
|
provider: 'codex',
|
|
165
70
|
...(config.effort ? { effort: config.effort } : {}),
|
|
166
71
|
...(config.model ? { model: config.model } : {}),
|
|
72
|
+
...(verbose === undefined ? {} : { verbose }),
|
|
167
73
|
workspaceRoot: config.configDir,
|
|
168
74
|
})
|
|
169
75
|
}
|
|
@@ -172,10 +78,22 @@ function createProvider(config: BatchConfig): BatchStructuredOutputProvider {
|
|
|
172
78
|
provider: 'claude',
|
|
173
79
|
...(config.effort ? { effort: config.effort } : {}),
|
|
174
80
|
...(config.model ? { model: config.model } : {}),
|
|
81
|
+
...(verbose === undefined ? {} : { verbose }),
|
|
175
82
|
workspaceRoot: config.configDir,
|
|
176
83
|
})
|
|
177
84
|
}
|
|
178
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
|
+
|
|
179
97
|
export async function runBatchCommand(
|
|
180
98
|
input: RunBatchCommandInput,
|
|
181
99
|
): Promise<RunBatchCommandResult> {
|
|
@@ -185,82 +103,68 @@ export async function runBatchCommand(
|
|
|
185
103
|
cwd,
|
|
186
104
|
})
|
|
187
105
|
|
|
188
|
-
const statePath = path.join(config.configDir, 'state.json')
|
|
189
106
|
const resultsPath = path.join(config.configDir, 'results.json')
|
|
190
|
-
const excludedFiles = new Set([config.configPath,
|
|
107
|
+
const excludedFiles = new Set([config.configPath, resultsPath])
|
|
191
108
|
const discoveredFiles = await discoverBatchFiles({
|
|
192
109
|
baseDir: config.configDir,
|
|
193
110
|
excludedFiles,
|
|
194
111
|
patterns: config.glob,
|
|
195
112
|
})
|
|
196
113
|
const results = await loadBatchResults(resultsPath)
|
|
197
|
-
|
|
198
|
-
|
|
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,
|
|
199
128
|
results,
|
|
200
|
-
|
|
129
|
+
resultsPath,
|
|
130
|
+
validateOutput,
|
|
201
131
|
})
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
132
|
+
|
|
133
|
+
const scheduler = createBatchRetryScheduler({
|
|
134
|
+
files: discoveredFiles,
|
|
135
|
+
protocol,
|
|
136
|
+
results,
|
|
137
|
+
store,
|
|
207
138
|
})
|
|
208
|
-
const validateOutput = ajv.compile(config.schema)
|
|
209
|
-
const processedFiles: string[] = []
|
|
210
|
-
let provider: BatchStructuredOutputProvider | null = null
|
|
211
139
|
|
|
212
|
-
|
|
213
|
-
if (state.pending.length === 0) {
|
|
214
|
-
state = await recycleFailedFiles(statePath, state)
|
|
215
|
-
continue
|
|
216
|
-
}
|
|
217
|
-
const filePath = state.pending[0]!
|
|
218
|
-
state = {
|
|
219
|
-
...state,
|
|
220
|
-
inProgress: unique([...state.inProgress, filePath]),
|
|
221
|
-
pending: state.pending.slice(1),
|
|
222
|
-
}
|
|
223
|
-
await writeJsonAtomic(statePath, state)
|
|
140
|
+
const processedFiles: string[] = []
|
|
224
141
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
...state,
|
|
242
|
-
inProgress: removeFile(state.inProgress, filePath),
|
|
243
|
-
}
|
|
244
|
-
await writeJsonAtomic(statePath, state)
|
|
245
|
-
processedFiles.push(filePath)
|
|
246
|
-
} catch (error) {
|
|
247
|
-
if (input.verbose) {
|
|
248
|
-
writeBatchFailure(filePath, error)
|
|
249
|
-
}
|
|
250
|
-
state = {
|
|
251
|
-
failed: unique([...state.failed, filePath]),
|
|
252
|
-
inProgress: removeFile(state.inProgress, filePath),
|
|
253
|
-
pending: state.pending,
|
|
254
|
-
}
|
|
255
|
-
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)
|
|
256
158
|
}
|
|
257
159
|
}
|
|
258
160
|
|
|
161
|
+
const sets = await scheduler.rebuild()
|
|
162
|
+
|
|
259
163
|
return {
|
|
260
164
|
config,
|
|
261
|
-
failedFiles:
|
|
165
|
+
failedFiles: [...sets.blocked],
|
|
262
166
|
processedFiles,
|
|
263
167
|
results,
|
|
264
|
-
|
|
168
|
+
resultsFilePath: resultsPath,
|
|
265
169
|
}
|
|
266
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
|
+
}
|