task-while 0.0.2 → 0.0.4

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.
Files changed (41) hide show
  1. package/README.md +34 -34
  2. package/package.json +2 -2
  3. package/src/adapters/fs/harness-store.ts +84 -0
  4. package/src/agents/claude.ts +159 -9
  5. package/src/agents/codex.ts +68 -4
  6. package/src/agents/event-log.ts +160 -15
  7. package/src/batch/discovery.ts +1 -1
  8. package/src/commands/batch.ts +152 -155
  9. package/src/commands/run-branch-helpers.ts +81 -0
  10. package/src/commands/run-providers.ts +77 -0
  11. package/src/commands/run.ts +121 -177
  12. package/src/core/create-runtime-ports.ts +118 -0
  13. package/src/core/runtime.ts +15 -36
  14. package/src/harness/in-memory-store.ts +45 -0
  15. package/src/harness/kernel.ts +226 -0
  16. package/src/harness/state.ts +47 -0
  17. package/src/harness/store.ts +26 -0
  18. package/src/harness/workflow-builders.ts +87 -0
  19. package/src/harness/workflow-program.ts +86 -0
  20. package/src/ports/agent.ts +17 -0
  21. package/src/ports/code-host.ts +23 -0
  22. package/src/programs/batch.ts +139 -0
  23. package/src/programs/run-direct.ts +209 -0
  24. package/src/programs/run-pr-transitions.ts +81 -0
  25. package/src/programs/run-pr.ts +290 -0
  26. package/src/programs/shared-steps.ts +252 -0
  27. package/src/schedulers/scheduler.ts +208 -0
  28. package/src/session/session.ts +127 -0
  29. package/src/workflow/config.ts +15 -0
  30. package/src/core/engine-helpers.ts +0 -114
  31. package/src/core/engine-outcomes.ts +0 -166
  32. package/src/core/engine.ts +0 -223
  33. package/src/core/orchestrator-helpers.ts +0 -52
  34. package/src/core/orchestrator-integrate-resume.ts +0 -149
  35. package/src/core/orchestrator-review-resume.ts +0 -228
  36. package/src/core/orchestrator-task-attempt.ts +0 -257
  37. package/src/core/orchestrator.ts +0 -99
  38. package/src/runtime/fs-runtime.ts +0 -209
  39. package/src/workflow/direct-preset.ts +0 -44
  40. package/src/workflow/preset.ts +0 -86
  41. 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
- }
@@ -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 }