task-while 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/bin/task-while.mjs +22 -0
- package/package.json +72 -0
- package/src/agents/claude.ts +175 -0
- package/src/agents/codex.ts +231 -0
- package/src/agents/provider-options.ts +45 -0
- package/src/agents/types.ts +69 -0
- package/src/batch/config.ts +109 -0
- package/src/batch/discovery.ts +35 -0
- package/src/batch/provider.ts +79 -0
- package/src/commands/batch.ts +266 -0
- package/src/commands/run.ts +270 -0
- package/src/core/engine-helpers.ts +114 -0
- package/src/core/engine-outcomes.ts +166 -0
- package/src/core/engine.ts +223 -0
- package/src/core/orchestrator-helpers.ts +52 -0
- package/src/core/orchestrator-integrate-resume.ts +149 -0
- package/src/core/orchestrator-review-resume.ts +228 -0
- package/src/core/orchestrator-task-attempt.ts +257 -0
- package/src/core/orchestrator.ts +99 -0
- package/src/core/runtime.ts +175 -0
- package/src/core/task-topology.ts +85 -0
- package/src/index.ts +121 -0
- package/src/prompts/implementer.ts +18 -0
- package/src/prompts/reviewer.ts +26 -0
- package/src/runtime/fs-runtime.ts +209 -0
- package/src/runtime/git.ts +137 -0
- package/src/runtime/github-pr-snapshot-decode.ts +307 -0
- package/src/runtime/github-pr-snapshot-queries.ts +137 -0
- package/src/runtime/github-pr-snapshot.ts +139 -0
- package/src/runtime/github.ts +232 -0
- package/src/runtime/path-layout.ts +13 -0
- package/src/runtime/workspace-resolver.ts +125 -0
- package/src/schema/index.ts +127 -0
- package/src/schema/model.ts +233 -0
- package/src/schema/shared.ts +93 -0
- package/src/task-sources/openspec/cli-json.ts +79 -0
- package/src/task-sources/openspec/context-files.ts +121 -0
- package/src/task-sources/openspec/parse-tasks-md.ts +57 -0
- package/src/task-sources/openspec/session.ts +235 -0
- package/src/task-sources/openspec/source.ts +59 -0
- package/src/task-sources/registry.ts +22 -0
- package/src/task-sources/spec-kit/parse-tasks-md.ts +48 -0
- package/src/task-sources/spec-kit/session.ts +174 -0
- package/src/task-sources/spec-kit/source.ts +30 -0
- package/src/task-sources/types.ts +47 -0
- package/src/types.ts +29 -0
- package/src/utils/fs.ts +31 -0
- package/src/workflow/config.ts +127 -0
- package/src/workflow/direct-preset.ts +44 -0
- package/src/workflow/finalize-task-checkbox.ts +24 -0
- package/src/workflow/preset.ts +86 -0
- package/src/workflow/pull-request-preset.ts +312 -0
- package/src/workflow/remote-reviewer.ts +243 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { parse } from 'yaml'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
claudeProviderOptionsSchema,
|
|
9
|
+
codexProviderOptionsSchema,
|
|
10
|
+
type WorkflowRoleProviderOptions,
|
|
11
|
+
} from '../agents/provider-options'
|
|
12
|
+
|
|
13
|
+
const workflowModeSchema = z.enum(['direct', 'pull-request'])
|
|
14
|
+
|
|
15
|
+
const defaultWorkflowRole: WorkflowRoleProviderOptions = {
|
|
16
|
+
provider: 'codex',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const workflowRoleProviderSchema = z.discriminatedUnion('provider', [
|
|
20
|
+
z
|
|
21
|
+
.object({
|
|
22
|
+
provider: z.literal('claude'),
|
|
23
|
+
})
|
|
24
|
+
.extend(claudeProviderOptionsSchema.shape)
|
|
25
|
+
.strict(),
|
|
26
|
+
z
|
|
27
|
+
.object({
|
|
28
|
+
provider: z.literal('codex'),
|
|
29
|
+
})
|
|
30
|
+
.extend(codexProviderOptionsSchema.shape)
|
|
31
|
+
.strict(),
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const workflowRoleSchema = z.preprocess((value) => {
|
|
35
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
36
|
+
const role = value as Record<string, unknown>
|
|
37
|
+
if (!('provider' in role)) {
|
|
38
|
+
return {
|
|
39
|
+
...role,
|
|
40
|
+
provider: 'codex',
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return value
|
|
45
|
+
}, workflowRoleProviderSchema)
|
|
46
|
+
|
|
47
|
+
const workflowRolesSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
implementer: workflowRoleSchema.default(defaultWorkflowRole),
|
|
50
|
+
reviewer: workflowRoleSchema.default(defaultWorkflowRole),
|
|
51
|
+
})
|
|
52
|
+
.strict()
|
|
53
|
+
|
|
54
|
+
const taskConfigSchema = z
|
|
55
|
+
.object({
|
|
56
|
+
maxIterations: z.number().int().min(1).max(20).default(5),
|
|
57
|
+
source: z.string().trim().min(1).default('spec-kit'),
|
|
58
|
+
})
|
|
59
|
+
.strict()
|
|
60
|
+
|
|
61
|
+
const workflowConfigSchema = z
|
|
62
|
+
.object({
|
|
63
|
+
task: taskConfigSchema.default({}),
|
|
64
|
+
workflow: z
|
|
65
|
+
.object({
|
|
66
|
+
mode: workflowModeSchema.default('direct'),
|
|
67
|
+
roles: workflowRolesSchema.default({}),
|
|
68
|
+
})
|
|
69
|
+
.strict()
|
|
70
|
+
.default({}),
|
|
71
|
+
})
|
|
72
|
+
.strict()
|
|
73
|
+
.default({})
|
|
74
|
+
|
|
75
|
+
export type WorkflowMode = 'direct' | 'pull-request'
|
|
76
|
+
|
|
77
|
+
export type WorkflowProvider = WorkflowRoleProviderOptions['provider']
|
|
78
|
+
export type WorkflowRoleConfig = WorkflowRoleProviderOptions
|
|
79
|
+
|
|
80
|
+
export interface WorkflowRolesConfig {
|
|
81
|
+
implementer: WorkflowRoleConfig
|
|
82
|
+
reviewer: WorkflowRoleConfig
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface WorkflowSettingsConfig {
|
|
86
|
+
mode: WorkflowMode
|
|
87
|
+
roles: WorkflowRolesConfig
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface TaskSettingsConfig {
|
|
91
|
+
maxIterations: number
|
|
92
|
+
source: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface WorkflowConfig {
|
|
96
|
+
task: TaskSettingsConfig
|
|
97
|
+
workflow: WorkflowSettingsConfig
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function loadWorkflowConfig(
|
|
101
|
+
workspaceRoot: string,
|
|
102
|
+
): Promise<WorkflowConfig> {
|
|
103
|
+
const configPath = path.join(workspaceRoot, 'while.yaml')
|
|
104
|
+
let rawConfig: unknown = {}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const configSource = await readFile(configPath, 'utf8')
|
|
108
|
+
rawConfig = parse(configSource) ?? {}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
111
|
+
throw error
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parsedConfig = workflowConfigSchema.parse(rawConfig)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
task: {
|
|
119
|
+
maxIterations: parsedConfig.task.maxIterations,
|
|
120
|
+
source: parsedConfig.task.source,
|
|
121
|
+
},
|
|
122
|
+
workflow: {
|
|
123
|
+
mode: parsedConfig.workflow.mode,
|
|
124
|
+
roles: parsedConfig.workflow.roles,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OrchestratorRuntime } from '../core/runtime'
|
|
2
|
+
|
|
3
|
+
export interface FinalizeTaskCheckboxInput {
|
|
4
|
+
commitMessage: string
|
|
5
|
+
runtime: OrchestratorRuntime
|
|
6
|
+
taskHandle: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function finalizeTaskCheckbox(input: FinalizeTaskCheckboxInput) {
|
|
10
|
+
try {
|
|
11
|
+
await input.runtime.taskSource.applyTaskCompletion(input.taskHandle)
|
|
12
|
+
return await input.runtime.git.commitTask({
|
|
13
|
+
message: input.commitMessage,
|
|
14
|
+
})
|
|
15
|
+
} catch (error) {
|
|
16
|
+
let reason = `Task commit failed: ${error instanceof Error ? error.message : String(error)}`
|
|
17
|
+
try {
|
|
18
|
+
await input.runtime.taskSource.revertTaskCompletion(input.taskHandle)
|
|
19
|
+
} catch (rollbackError) {
|
|
20
|
+
reason = `${reason}; task completion rollback failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
21
|
+
}
|
|
22
|
+
throw new Error(reason)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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'
|
|
@@ -0,0 +1,312 @@
|
|
|
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 }
|