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
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { appendFile, readFile } from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
import * as fsExtra from 'fs-extra'
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
validateFinalReport,
|
|
8
|
-
validateImplementArtifact,
|
|
9
|
-
validateIntegrateArtifact,
|
|
10
|
-
validateReviewArtifact,
|
|
11
|
-
validateTaskGraph,
|
|
12
|
-
validateWorkflowEvent,
|
|
13
|
-
validateWorkflowState,
|
|
14
|
-
} from '../schema/index'
|
|
15
|
-
import { writeJsonAtomic } from '../utils/fs'
|
|
16
|
-
import { GitRuntime } from './git'
|
|
17
|
-
import { GitHubRuntime } from './github'
|
|
18
|
-
import { createRuntimePaths } from './path-layout'
|
|
19
|
-
|
|
20
|
-
import type { AttemptArtifactKey, OrchestratorRuntime } from '../core/runtime'
|
|
21
|
-
import type { TaskSourceSession } from '../task-sources/types'
|
|
22
|
-
|
|
23
|
-
function createArtifactDir(
|
|
24
|
-
featureDir: string,
|
|
25
|
-
taskId: string,
|
|
26
|
-
generation: number,
|
|
27
|
-
attempt: number,
|
|
28
|
-
) {
|
|
29
|
-
const runtimePaths = createRuntimePaths(featureDir)
|
|
30
|
-
return path.join(
|
|
31
|
-
runtimePaths.tasksDir,
|
|
32
|
-
taskId,
|
|
33
|
-
`g${generation}`,
|
|
34
|
-
`a${attempt}`,
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function readTextFileIfExists(filePath: string) {
|
|
39
|
-
const exists = await fsExtra.pathExists(filePath)
|
|
40
|
-
if (!exists) {
|
|
41
|
-
return null
|
|
42
|
-
}
|
|
43
|
-
return readFile(filePath, 'utf8')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function readValidatedJsonFileIfExists<T>(
|
|
47
|
-
filePath: string,
|
|
48
|
-
validate: (value: unknown) => T,
|
|
49
|
-
): Promise<null | T> {
|
|
50
|
-
const raw = await readTextFileIfExists(filePath)
|
|
51
|
-
if (raw === null) {
|
|
52
|
-
return null
|
|
53
|
-
}
|
|
54
|
-
return validate(JSON.parse(raw))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface CreateOrchestratorRuntimeInput {
|
|
58
|
-
featureDir: string
|
|
59
|
-
taskSource?: TaskSourceSession
|
|
60
|
-
workspaceRoot: string
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function createOrchestratorRuntime(
|
|
64
|
-
input: CreateOrchestratorRuntimeInput,
|
|
65
|
-
): OrchestratorRuntime {
|
|
66
|
-
const runtimePaths = createRuntimePaths(input.featureDir)
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
git: new GitRuntime(input.workspaceRoot, runtimePaths.runtimeDir),
|
|
70
|
-
github: new GitHubRuntime(input.workspaceRoot),
|
|
71
|
-
store: {
|
|
72
|
-
async appendEvent(event) {
|
|
73
|
-
const value = validateWorkflowEvent(event)
|
|
74
|
-
await fsExtra.ensureDir(path.dirname(runtimePaths.events))
|
|
75
|
-
await appendFile(runtimePaths.events, `${JSON.stringify(value)}\n`)
|
|
76
|
-
},
|
|
77
|
-
async loadGraph() {
|
|
78
|
-
return readValidatedJsonFileIfExists(
|
|
79
|
-
runtimePaths.graph,
|
|
80
|
-
validateTaskGraph,
|
|
81
|
-
)
|
|
82
|
-
},
|
|
83
|
-
async loadImplementArtifact(key: AttemptArtifactKey) {
|
|
84
|
-
const filePath = path.join(
|
|
85
|
-
createArtifactDir(
|
|
86
|
-
input.featureDir,
|
|
87
|
-
key.taskHandle,
|
|
88
|
-
key.generation,
|
|
89
|
-
key.attempt,
|
|
90
|
-
),
|
|
91
|
-
'implement.json',
|
|
92
|
-
)
|
|
93
|
-
return readValidatedJsonFileIfExists(
|
|
94
|
-
filePath,
|
|
95
|
-
validateImplementArtifact,
|
|
96
|
-
)
|
|
97
|
-
},
|
|
98
|
-
async loadReviewArtifact(key: AttemptArtifactKey) {
|
|
99
|
-
const filePath = path.join(
|
|
100
|
-
createArtifactDir(
|
|
101
|
-
input.featureDir,
|
|
102
|
-
key.taskHandle,
|
|
103
|
-
key.generation,
|
|
104
|
-
key.attempt,
|
|
105
|
-
),
|
|
106
|
-
'review.json',
|
|
107
|
-
)
|
|
108
|
-
return readValidatedJsonFileIfExists(filePath, validateReviewArtifact)
|
|
109
|
-
},
|
|
110
|
-
async loadState() {
|
|
111
|
-
return readValidatedJsonFileIfExists(
|
|
112
|
-
runtimePaths.state,
|
|
113
|
-
validateWorkflowState,
|
|
114
|
-
)
|
|
115
|
-
},
|
|
116
|
-
async readReport() {
|
|
117
|
-
return readValidatedJsonFileIfExists(
|
|
118
|
-
runtimePaths.report,
|
|
119
|
-
validateFinalReport,
|
|
120
|
-
)
|
|
121
|
-
},
|
|
122
|
-
async reset() {
|
|
123
|
-
await fsExtra.remove(runtimePaths.runtimeDir)
|
|
124
|
-
},
|
|
125
|
-
async saveGraph(graph) {
|
|
126
|
-
await writeJsonAtomic(runtimePaths.graph, validateTaskGraph(graph))
|
|
127
|
-
},
|
|
128
|
-
async saveImplementArtifact(artifact) {
|
|
129
|
-
const value = validateImplementArtifact(artifact)
|
|
130
|
-
const targetPath = path.join(
|
|
131
|
-
createArtifactDir(
|
|
132
|
-
input.featureDir,
|
|
133
|
-
artifact.taskHandle,
|
|
134
|
-
artifact.generation,
|
|
135
|
-
artifact.attempt,
|
|
136
|
-
),
|
|
137
|
-
'implement.json',
|
|
138
|
-
)
|
|
139
|
-
await writeJsonAtomic(targetPath, value)
|
|
140
|
-
},
|
|
141
|
-
async saveIntegrateArtifact(artifact) {
|
|
142
|
-
const value = validateIntegrateArtifact(artifact)
|
|
143
|
-
const targetPath = path.join(
|
|
144
|
-
createArtifactDir(
|
|
145
|
-
input.featureDir,
|
|
146
|
-
artifact.taskHandle,
|
|
147
|
-
artifact.generation,
|
|
148
|
-
artifact.attempt,
|
|
149
|
-
),
|
|
150
|
-
'integrate.json',
|
|
151
|
-
)
|
|
152
|
-
await writeJsonAtomic(targetPath, value)
|
|
153
|
-
},
|
|
154
|
-
async saveReport(report) {
|
|
155
|
-
await writeJsonAtomic(runtimePaths.report, validateFinalReport(report))
|
|
156
|
-
},
|
|
157
|
-
async saveReviewArtifact(artifact) {
|
|
158
|
-
const value = validateReviewArtifact(artifact)
|
|
159
|
-
const targetPath = path.join(
|
|
160
|
-
createArtifactDir(
|
|
161
|
-
input.featureDir,
|
|
162
|
-
artifact.taskHandle,
|
|
163
|
-
artifact.generation,
|
|
164
|
-
artifact.attempt,
|
|
165
|
-
),
|
|
166
|
-
'review.json',
|
|
167
|
-
)
|
|
168
|
-
await writeJsonAtomic(targetPath, value)
|
|
169
|
-
},
|
|
170
|
-
async saveState(state) {
|
|
171
|
-
await writeJsonAtomic(runtimePaths.state, validateWorkflowState(state))
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
taskSource:
|
|
175
|
-
input.taskSource ??
|
|
176
|
-
({
|
|
177
|
-
async applyTaskCompletion() {
|
|
178
|
-
throw new Error('task source is not configured')
|
|
179
|
-
},
|
|
180
|
-
buildCommitSubject() {
|
|
181
|
-
throw new Error('task source is not configured')
|
|
182
|
-
},
|
|
183
|
-
async buildImplementPrompt() {
|
|
184
|
-
throw new Error('task source is not configured')
|
|
185
|
-
},
|
|
186
|
-
async buildReviewPrompt() {
|
|
187
|
-
throw new Error('task source is not configured')
|
|
188
|
-
},
|
|
189
|
-
async getCompletionCriteria() {
|
|
190
|
-
throw new Error('task source is not configured')
|
|
191
|
-
},
|
|
192
|
-
getTaskDependencies() {
|
|
193
|
-
throw new Error('task source is not configured')
|
|
194
|
-
},
|
|
195
|
-
async isTaskCompleted() {
|
|
196
|
-
throw new Error('task source is not configured')
|
|
197
|
-
},
|
|
198
|
-
listTasks() {
|
|
199
|
-
throw new Error('task source is not configured')
|
|
200
|
-
},
|
|
201
|
-
resolveTaskSelector() {
|
|
202
|
-
throw new Error('task source is not configured')
|
|
203
|
-
},
|
|
204
|
-
async revertTaskCompletion() {
|
|
205
|
-
throw new Error('task source is not configured')
|
|
206
|
-
},
|
|
207
|
-
} satisfies TaskSourceSession),
|
|
208
|
-
}
|
|
209
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { finalizeTaskCheckbox } from './finalize-task-checkbox'
|
|
2
|
-
|
|
3
|
-
import type { ReviewerProvider } from '../agents/types'
|
|
4
|
-
import type { DirectWorkflowPreset } from './preset'
|
|
5
|
-
|
|
6
|
-
export interface CreateDirectWorkflowPresetInput {
|
|
7
|
-
reviewer: ReviewerProvider
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function createDirectWorkflowPreset(
|
|
11
|
-
input: CreateDirectWorkflowPresetInput,
|
|
12
|
-
): DirectWorkflowPreset {
|
|
13
|
-
return {
|
|
14
|
-
mode: 'direct',
|
|
15
|
-
async integrate(context) {
|
|
16
|
-
const { commitSha } = await finalizeTaskCheckbox({
|
|
17
|
-
commitMessage: context.commitMessage,
|
|
18
|
-
runtime: context.runtime,
|
|
19
|
-
taskHandle: context.taskHandle,
|
|
20
|
-
})
|
|
21
|
-
return {
|
|
22
|
-
kind: 'completed',
|
|
23
|
-
result: {
|
|
24
|
-
commitSha,
|
|
25
|
-
summary: 'integrated',
|
|
26
|
-
},
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
async review(context) {
|
|
30
|
-
const review = await input.reviewer.review({
|
|
31
|
-
actualChangedFiles: context.actualChangedFiles,
|
|
32
|
-
attempt: context.attempt,
|
|
33
|
-
generation: context.generation,
|
|
34
|
-
implement: context.implement,
|
|
35
|
-
lastFindings: context.lastFindings,
|
|
36
|
-
prompt: context.prompt,
|
|
37
|
-
taskHandle: context.taskHandle,
|
|
38
|
-
})
|
|
39
|
-
return review.verdict === 'pass'
|
|
40
|
-
? { kind: 'approved', review }
|
|
41
|
-
: { kind: 'rejected', review }
|
|
42
|
-
},
|
|
43
|
-
}
|
|
44
|
-
}
|
package/src/workflow/preset.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type { WorkflowRoleProviders } from '../agents/types'
|
|
2
|
-
import type { OrchestratorRuntime } from '../core/runtime'
|
|
3
|
-
import type { TaskPrompt } from '../task-sources/types'
|
|
4
|
-
import type { ImplementOutput, ReviewFinding, ReviewOutput } from '../types'
|
|
5
|
-
|
|
6
|
-
export type WorkflowMode = 'direct' | 'pull-request'
|
|
7
|
-
|
|
8
|
-
export interface DirectReviewPhaseContext {
|
|
9
|
-
actualChangedFiles: string[]
|
|
10
|
-
attempt: number
|
|
11
|
-
commitMessage: string
|
|
12
|
-
generation: number
|
|
13
|
-
implement: ImplementOutput
|
|
14
|
-
lastFindings: ReviewFinding[]
|
|
15
|
-
prompt: TaskPrompt
|
|
16
|
-
taskHandle: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface PullRequestReviewPhaseContext {
|
|
20
|
-
attempt: number
|
|
21
|
-
commitMessage: string
|
|
22
|
-
completionCriteria: string[]
|
|
23
|
-
generation?: number
|
|
24
|
-
implement?: ImplementOutput
|
|
25
|
-
lastFindings?: never[]
|
|
26
|
-
runtime: OrchestratorRuntime
|
|
27
|
-
taskHandle: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ApprovedReviewPhaseResult {
|
|
31
|
-
kind: 'approved'
|
|
32
|
-
review: ReviewOutput
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface RejectedReviewPhaseResult {
|
|
36
|
-
kind: 'rejected'
|
|
37
|
-
review: ReviewOutput
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type ReviewPhaseResult =
|
|
41
|
-
| ApprovedReviewPhaseResult
|
|
42
|
-
| RejectedReviewPhaseResult
|
|
43
|
-
|
|
44
|
-
export interface IntegratePhaseContext {
|
|
45
|
-
commitMessage: string
|
|
46
|
-
runtime: OrchestratorRuntime
|
|
47
|
-
taskHandle: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface IntegratePhaseResult {
|
|
51
|
-
kind: 'completed'
|
|
52
|
-
result: IntegrateResult
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface IntegrateResult {
|
|
56
|
-
commitSha: string
|
|
57
|
-
summary: string
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface DirectWorkflowPreset {
|
|
61
|
-
integrate: (context: IntegratePhaseContext) => Promise<IntegratePhaseResult>
|
|
62
|
-
readonly mode: 'direct'
|
|
63
|
-
review: (context: DirectReviewPhaseContext) => Promise<ReviewPhaseResult>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface PullRequestWorkflowPreset {
|
|
67
|
-
integrate: (context: IntegratePhaseContext) => Promise<IntegratePhaseResult>
|
|
68
|
-
readonly mode: 'pull-request'
|
|
69
|
-
review: (context: PullRequestReviewPhaseContext) => Promise<ReviewPhaseResult>
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type WorkflowPreset = DirectWorkflowPreset | PullRequestWorkflowPreset
|
|
73
|
-
|
|
74
|
-
export function isPullRequestWorkflowPreset(
|
|
75
|
-
preset: WorkflowPreset,
|
|
76
|
-
): preset is PullRequestWorkflowPreset {
|
|
77
|
-
return preset.mode === 'pull-request'
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface WorkflowRuntime {
|
|
81
|
-
preset: WorkflowPreset
|
|
82
|
-
roles: WorkflowRoleProviders
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export { createDirectWorkflowPreset } from './direct-preset'
|
|
86
|
-
export { createPullRequestWorkflowPreset } from './pull-request-preset'
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import { finalizeTaskCheckbox } from './finalize-task-checkbox'
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
PullRequestReviewResult,
|
|
5
|
-
RemoteReviewerProvider,
|
|
6
|
-
} from '../agents/types'
|
|
7
|
-
import type { OrchestratorRuntime, PullRequestRef } from '../core/runtime'
|
|
8
|
-
import type {
|
|
9
|
-
IntegratePhaseResult,
|
|
10
|
-
PullRequestReviewPhaseContext,
|
|
11
|
-
PullRequestWorkflowPreset,
|
|
12
|
-
ReviewPhaseResult,
|
|
13
|
-
} from './preset'
|
|
14
|
-
|
|
15
|
-
const DEFAULT_BASE_BRANCH = 'main'
|
|
16
|
-
const DEFAULT_REVIEW_POLL_INTERVAL_MS = 60_000
|
|
17
|
-
|
|
18
|
-
function toTaskBranchName(commitMessage: string) {
|
|
19
|
-
const slug = commitMessage
|
|
20
|
-
.replace(/^Task\s+/i, '')
|
|
21
|
-
.toLowerCase()
|
|
22
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
23
|
-
.replace(/^-+|-+$/g, '')
|
|
24
|
-
return `task/${slug}`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function createCheckpointCommitMessage(commitMessage: string, attempt: number) {
|
|
28
|
-
return `checkpoint: ${commitMessage} (attempt ${attempt})`
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function createPullRequestBody(context: PullRequestReviewPhaseContext) {
|
|
32
|
-
return [
|
|
33
|
-
`Task: ${context.commitMessage}`,
|
|
34
|
-
`Attempt: ${context.attempt}`,
|
|
35
|
-
'',
|
|
36
|
-
'Managed by task-while.',
|
|
37
|
-
].join('\n')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface EnsureTaskBranchInput {
|
|
41
|
-
branchName: string
|
|
42
|
-
restoreFromRemote: boolean
|
|
43
|
-
runtime: OrchestratorRuntime
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function ensureTaskBranch(input: EnsureTaskBranchInput) {
|
|
47
|
-
const currentBranch = await input.runtime.git.getCurrentBranch()
|
|
48
|
-
if (currentBranch === input.branchName) {
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
await input.runtime.git.checkoutBranch(input.branchName)
|
|
53
|
-
} catch {
|
|
54
|
-
if (input.restoreFromRemote) {
|
|
55
|
-
await input.runtime.git.checkoutRemoteBranch(input.branchName)
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
await input.runtime.git.checkoutBranch(input.branchName, {
|
|
59
|
-
create: true,
|
|
60
|
-
startPoint: DEFAULT_BASE_BRANCH,
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface EnsurePullRequestInput {
|
|
66
|
-
branchName: string
|
|
67
|
-
branchNeedsPush: boolean
|
|
68
|
-
context: PullRequestReviewPhaseContext
|
|
69
|
-
existingPullRequest: null | PullRequestRef
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function ensurePullRequest(
|
|
73
|
-
input: EnsurePullRequestInput,
|
|
74
|
-
): Promise<PullRequestRef> {
|
|
75
|
-
let pullRequest = input.existingPullRequest
|
|
76
|
-
|
|
77
|
-
if (input.branchNeedsPush || !pullRequest) {
|
|
78
|
-
await input.context.runtime.git.pushBranch(input.branchName)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (pullRequest) {
|
|
82
|
-
return pullRequest
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
pullRequest = await input.context.runtime.github.createPullRequest({
|
|
86
|
-
baseBranch: DEFAULT_BASE_BRANCH,
|
|
87
|
-
body: createPullRequestBody(input.context),
|
|
88
|
-
headBranch: input.branchName,
|
|
89
|
-
title: input.context.commitMessage,
|
|
90
|
-
})
|
|
91
|
-
return pullRequest
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface WaitForRemoteReviewInput {
|
|
95
|
-
checkpointStartedAt: string
|
|
96
|
-
context: PullRequestReviewPhaseContext
|
|
97
|
-
pullRequest: PullRequestRef
|
|
98
|
-
reviewer: RemoteReviewerProvider
|
|
99
|
-
sleep: SleepFunction
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
type SleepFunction = (ms: number) => Promise<void>
|
|
103
|
-
|
|
104
|
-
async function waitForRemoteReview(
|
|
105
|
-
input: WaitForRemoteReviewInput,
|
|
106
|
-
): Promise<ReviewPhaseResult> {
|
|
107
|
-
const evaluateReview = async () => {
|
|
108
|
-
const snapshot = await input.context.runtime.github.getPullRequestSnapshot({
|
|
109
|
-
pullRequestNumber: input.pullRequest.number,
|
|
110
|
-
})
|
|
111
|
-
return input.reviewer.evaluatePullRequestReview({
|
|
112
|
-
checkpointStartedAt: input.checkpointStartedAt,
|
|
113
|
-
completionCriteria: input.context.completionCriteria,
|
|
114
|
-
pullRequest: snapshot,
|
|
115
|
-
taskHandle: input.context.taskHandle,
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let result: PullRequestReviewResult = await evaluateReview()
|
|
120
|
-
while (result.kind === 'pending') {
|
|
121
|
-
await input.sleep(DEFAULT_REVIEW_POLL_INTERVAL_MS)
|
|
122
|
-
result = await evaluateReview()
|
|
123
|
-
}
|
|
124
|
-
return result
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function toLocalCleanupWarning(error: unknown) {
|
|
128
|
-
return error instanceof Error ? error.message : String(error)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
interface CleanupAfterMergeInput {
|
|
132
|
-
branchName: string
|
|
133
|
-
runtime: OrchestratorRuntime
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function cleanupAfterMerge(input: CleanupAfterMergeInput) {
|
|
137
|
-
const warnings: string[] = []
|
|
138
|
-
let onBaseBranch = false
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
await input.runtime.git.checkoutBranch(DEFAULT_BASE_BRANCH)
|
|
142
|
-
onBaseBranch = true
|
|
143
|
-
} catch (error) {
|
|
144
|
-
warnings.push(
|
|
145
|
-
`checkout ${DEFAULT_BASE_BRANCH} failed: ${toLocalCleanupWarning(error)}`,
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!onBaseBranch) {
|
|
150
|
-
return warnings
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
await input.runtime.git.pullFastForward(DEFAULT_BASE_BRANCH)
|
|
155
|
-
} catch (error) {
|
|
156
|
-
warnings.push(
|
|
157
|
-
`pull ${DEFAULT_BASE_BRANCH} failed: ${toLocalCleanupWarning(error)}`,
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
await input.runtime.git.deleteLocalBranch(input.branchName)
|
|
163
|
-
} catch (error) {
|
|
164
|
-
warnings.push(`delete local branch failed: ${toLocalCleanupWarning(error)}`)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return warnings
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
interface SummarizeIntegrateResultInput {
|
|
171
|
-
status: 'already integrated' | 'integrated'
|
|
172
|
-
warnings: string[]
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function summarizeIntegrateResult(input: SummarizeIntegrateResultInput) {
|
|
176
|
-
return input.warnings.length === 0
|
|
177
|
-
? input.status
|
|
178
|
-
: `${input.status}; local cleanup warning: ${input.warnings.join('; ')}`
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export interface CreatePullRequestWorkflowPresetInput {
|
|
182
|
-
reviewer: RemoteReviewerProvider
|
|
183
|
-
sleep?: SleepFunction
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function createPullRequestWorkflowPreset(
|
|
187
|
-
input: CreatePullRequestWorkflowPresetInput,
|
|
188
|
-
): PullRequestWorkflowPreset {
|
|
189
|
-
const sleep =
|
|
190
|
-
input.sleep ??
|
|
191
|
-
(async (ms: number) => {
|
|
192
|
-
await new Promise((resolve) => {
|
|
193
|
-
setTimeout(resolve, ms)
|
|
194
|
-
})
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
mode: 'pull-request',
|
|
199
|
-
async integrate(context): Promise<IntegratePhaseResult> {
|
|
200
|
-
const branchName = toTaskBranchName(context.commitMessage)
|
|
201
|
-
const openPullRequest =
|
|
202
|
-
await context.runtime.github.findOpenPullRequestByHeadBranch({
|
|
203
|
-
headBranch: branchName,
|
|
204
|
-
})
|
|
205
|
-
if (openPullRequest) {
|
|
206
|
-
await ensureTaskBranch({
|
|
207
|
-
branchName,
|
|
208
|
-
restoreFromRemote: true,
|
|
209
|
-
runtime: context.runtime,
|
|
210
|
-
})
|
|
211
|
-
const taskChecked = await context.runtime.taskSource.isTaskCompleted(
|
|
212
|
-
context.taskHandle,
|
|
213
|
-
)
|
|
214
|
-
if (!taskChecked) {
|
|
215
|
-
await finalizeTaskCheckbox({
|
|
216
|
-
commitMessage: context.commitMessage,
|
|
217
|
-
runtime: context.runtime,
|
|
218
|
-
taskHandle: context.taskHandle,
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
await context.runtime.git.pushBranch(branchName)
|
|
222
|
-
const mergeResult = await context.runtime.github.squashMergePullRequest(
|
|
223
|
-
{
|
|
224
|
-
pullRequestNumber: openPullRequest.number,
|
|
225
|
-
subject: context.commitMessage,
|
|
226
|
-
},
|
|
227
|
-
)
|
|
228
|
-
const warnings = await cleanupAfterMerge({
|
|
229
|
-
branchName,
|
|
230
|
-
runtime: context.runtime,
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
kind: 'completed',
|
|
235
|
-
result: {
|
|
236
|
-
commitSha: mergeResult.commitSha,
|
|
237
|
-
summary: summarizeIntegrateResult({
|
|
238
|
-
status: 'integrated',
|
|
239
|
-
warnings,
|
|
240
|
-
}),
|
|
241
|
-
},
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const mergedPullRequest =
|
|
246
|
-
await context.runtime.github.findMergedPullRequestByHeadBranch({
|
|
247
|
-
headBranch: branchName,
|
|
248
|
-
})
|
|
249
|
-
if (!mergedPullRequest) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`Missing open or merged pull request for branch ${branchName}`,
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
const warnings = await cleanupAfterMerge({
|
|
255
|
-
branchName,
|
|
256
|
-
runtime: context.runtime,
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
kind: 'completed',
|
|
261
|
-
result: {
|
|
262
|
-
commitSha: mergedPullRequest.mergeCommitSha,
|
|
263
|
-
summary: summarizeIntegrateResult({
|
|
264
|
-
status: 'already integrated',
|
|
265
|
-
warnings,
|
|
266
|
-
}),
|
|
267
|
-
},
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
async review(context): Promise<ReviewPhaseResult> {
|
|
271
|
-
const branchName = toTaskBranchName(context.commitMessage)
|
|
272
|
-
const checkpointMessage = createCheckpointCommitMessage(
|
|
273
|
-
context.commitMessage,
|
|
274
|
-
context.attempt,
|
|
275
|
-
)
|
|
276
|
-
const existingPullRequest =
|
|
277
|
-
await context.runtime.github.findOpenPullRequestByHeadBranch({
|
|
278
|
-
headBranch: branchName,
|
|
279
|
-
})
|
|
280
|
-
await ensureTaskBranch({
|
|
281
|
-
branchName,
|
|
282
|
-
restoreFromRemote: existingPullRequest !== null,
|
|
283
|
-
runtime: context.runtime,
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
const headSubject = await context.runtime.git.getHeadSubject()
|
|
287
|
-
if (headSubject !== checkpointMessage) {
|
|
288
|
-
await context.runtime.git.commitTask({
|
|
289
|
-
message: checkpointMessage,
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const checkpointStartedAt = await context.runtime.git.getHeadTimestamp()
|
|
294
|
-
const pullRequest = await ensurePullRequest({
|
|
295
|
-
branchName,
|
|
296
|
-
branchNeedsPush: true,
|
|
297
|
-
context,
|
|
298
|
-
existingPullRequest,
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
return waitForRemoteReview({
|
|
302
|
-
checkpointStartedAt,
|
|
303
|
-
context,
|
|
304
|
-
pullRequest,
|
|
305
|
-
reviewer: input.reviewer,
|
|
306
|
-
sleep,
|
|
307
|
-
})
|
|
308
|
-
},
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export { DEFAULT_REVIEW_POLL_INTERVAL_MS, toTaskBranchName }
|