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.
Files changed (42) hide show
  1. package/README.md +32 -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 +195 -0
  7. package/src/batch/discovery.ts +1 -1
  8. package/src/batch/provider.ts +9 -0
  9. package/src/commands/batch.ts +69 -165
  10. package/src/commands/run-branch-helpers.ts +81 -0
  11. package/src/commands/run-providers.ts +77 -0
  12. package/src/commands/run.ts +117 -225
  13. package/src/core/create-runtime-ports.ts +118 -0
  14. package/src/core/runtime.ts +15 -36
  15. package/src/harness/in-memory-store.ts +45 -0
  16. package/src/harness/kernel.ts +226 -0
  17. package/src/harness/state.ts +47 -0
  18. package/src/harness/store.ts +26 -0
  19. package/src/harness/workflow-builders.ts +87 -0
  20. package/src/harness/workflow-program.ts +86 -0
  21. package/src/ports/agent.ts +17 -0
  22. package/src/ports/code-host.ts +23 -0
  23. package/src/programs/batch.ts +139 -0
  24. package/src/programs/run-direct.ts +209 -0
  25. package/src/programs/run-pr-transitions.ts +81 -0
  26. package/src/programs/run-pr.ts +290 -0
  27. package/src/programs/shared-steps.ts +252 -0
  28. package/src/schedulers/scheduler.ts +208 -0
  29. package/src/session/session.ts +127 -0
  30. package/src/workflow/config.ts +15 -0
  31. package/src/core/engine-helpers.ts +0 -114
  32. package/src/core/engine-outcomes.ts +0 -166
  33. package/src/core/engine.ts +0 -223
  34. package/src/core/orchestrator-helpers.ts +0 -52
  35. package/src/core/orchestrator-integrate-resume.ts +0 -149
  36. package/src/core/orchestrator-review-resume.ts +0 -228
  37. package/src/core/orchestrator-task-attempt.ts +0 -257
  38. package/src/core/orchestrator.ts +0 -99
  39. package/src/runtime/fs-runtime.ts +0 -209
  40. package/src/workflow/direct-preset.ts +0 -44
  41. package/src/workflow/preset.ts +0 -86
  42. package/src/workflow/pull-request-preset.ts +0 -312
@@ -1,114 +0,0 @@
1
- import type {
2
- PendingTaskState,
3
- ReviewFinding,
4
- ReviewOutput,
5
- ReviewVerdict,
6
- TaskGraph,
7
- TaskState,
8
- TaskTopologyEntry,
9
- WorkflowState,
10
- } from '../types'
11
-
12
- export function cloneState(state: WorkflowState): WorkflowState {
13
- return structuredClone(state)
14
- }
15
-
16
- export function createBaseTaskState(): PendingTaskState {
17
- return {
18
- attempt: 0,
19
- generation: 1,
20
- invalidatedBy: null,
21
- lastFindings: [],
22
- status: 'pending',
23
- }
24
- }
25
-
26
- export function getTask(
27
- graph: TaskGraph,
28
- taskHandle: string,
29
- ): TaskTopologyEntry {
30
- const task = graph.tasks.find((item) => item.handle === taskHandle)
31
- if (!task) {
32
- throw new Error(`Unknown task: ${taskHandle}`)
33
- }
34
- return task
35
- }
36
-
37
- export function getTaskState(
38
- state: WorkflowState,
39
- taskHandle: string,
40
- ): TaskState {
41
- const taskState = state.tasks[taskHandle]
42
- if (!taskState) {
43
- throw new Error(`Missing state for task ${taskHandle}`)
44
- }
45
- return taskState
46
- }
47
-
48
- export function canStartTask(
49
- graph: TaskGraph,
50
- state: WorkflowState,
51
- taskHandle: string,
52
- ) {
53
- const task = getTask(graph, taskHandle)
54
- return task.dependsOn.every(
55
- (dependency) => state.tasks[dependency]?.status === 'done',
56
- )
57
- }
58
-
59
- export function getMaxIterations(graph: TaskGraph) {
60
- return graph.maxIterations
61
- }
62
-
63
- export function withReviewMetadata(
64
- taskState: TaskState,
65
- input: WithReviewMetadataInput,
66
- ) {
67
- const next = {
68
- attempt: taskState.attempt,
69
- generation: taskState.generation,
70
- invalidatedBy: taskState.invalidatedBy,
71
- lastFindings: taskState.lastFindings,
72
- ...(taskState.lastReviewVerdict
73
- ? { lastReviewVerdict: taskState.lastReviewVerdict }
74
- : {}),
75
- }
76
- if (input.findings) {
77
- next.lastFindings = input.findings
78
- }
79
- if (input.reviewVerdict) {
80
- next.lastReviewVerdict = input.reviewVerdict
81
- }
82
- return next
83
- }
84
-
85
- export interface WithReviewMetadataInput {
86
- findings?: ReviewFinding[]
87
- reviewVerdict?: ReviewVerdict
88
- }
89
-
90
- export interface ZeroGateInput {
91
- review: ReviewOutput
92
- }
93
-
94
- export function shouldPassZeroGate(input: ZeroGateInput) {
95
- return (
96
- input.review.verdict === 'pass' &&
97
- input.review.findings.length === 0 &&
98
- input.review.acceptanceChecks.every((check) => check.status === 'pass')
99
- )
100
- }
101
-
102
- export function collectDescendants(
103
- graph: TaskGraph,
104
- taskHandle: string,
105
- seen = new Set<string>(),
106
- ) {
107
- for (const task of graph.tasks) {
108
- if (task.dependsOn.includes(taskHandle) && !seen.has(task.handle)) {
109
- seen.add(task.handle)
110
- collectDescendants(graph, task.handle, seen)
111
- }
112
- }
113
- return seen
114
- }
@@ -1,166 +0,0 @@
1
- import {
2
- cloneState,
3
- getMaxIterations,
4
- getTaskState,
5
- shouldPassZeroGate,
6
- withReviewMetadata,
7
- } from './engine-helpers'
8
-
9
- import type {
10
- FinalReport,
11
- ReviewOutput,
12
- TaskGraph,
13
- WorkflowState,
14
- } from '../types'
15
-
16
- export function recordReviewApproved(
17
- state: WorkflowState,
18
- taskHandle: string,
19
- review: ReviewOutput,
20
- ): WorkflowState {
21
- const next = cloneState(state)
22
- const taskState = getTaskState(next, taskHandle)
23
- if (taskState.status !== 'running' || taskState.stage !== 'review') {
24
- throw new Error(`Task ${taskHandle} is not reviewing`)
25
- }
26
- if (review.verdict !== 'pass') {
27
- throw new Error(`Task ${taskHandle} review is not approved`)
28
- }
29
- next.tasks[taskHandle] = {
30
- ...withReviewMetadata(taskState, {
31
- findings: review.findings,
32
- reviewVerdict: review.verdict,
33
- }),
34
- stage: 'integrate',
35
- status: 'running',
36
- }
37
- return next
38
- }
39
-
40
- export function recordIntegrateResult(
41
- _graph: TaskGraph,
42
- state: WorkflowState,
43
- taskHandle: string,
44
- input: RecordIntegrateResultInput,
45
- ): WorkflowState {
46
- const next = cloneState(state)
47
- const taskState = getTaskState(next, taskHandle)
48
- if (taskState.status !== 'running' || taskState.stage !== 'integrate') {
49
- throw new Error(`Task ${taskHandle} is not integrating`)
50
- }
51
- if (!shouldPassZeroGate(input)) {
52
- throw new Error(
53
- `Task ${taskHandle} integration requires an approved review`,
54
- )
55
- }
56
- next.currentTaskHandle = null
57
- next.tasks[taskHandle] = {
58
- ...withReviewMetadata(taskState, {
59
- findings: input.review.findings,
60
- reviewVerdict: input.review.verdict,
61
- }),
62
- commitSha: input.commitSha,
63
- status: 'done',
64
- }
65
- return next
66
- }
67
-
68
- export interface RecordIntegrateResultInput {
69
- commitSha: string
70
- review: ReviewOutput
71
- }
72
-
73
- export function recordCommitFailure(
74
- graph: TaskGraph,
75
- state: WorkflowState,
76
- taskHandle: string,
77
- reason: string,
78
- ): WorkflowState {
79
- const next = cloneState(state)
80
- const taskState = getTaskState(next, taskHandle)
81
- next.currentTaskHandle = null
82
- const metadata = withReviewMetadata(taskState, {
83
- reviewVerdict: 'pass',
84
- })
85
- next.tasks[taskHandle] =
86
- taskState.attempt >= getMaxIterations(graph)
87
- ? {
88
- ...metadata,
89
- reason,
90
- status: 'blocked',
91
- }
92
- : {
93
- ...metadata,
94
- status: 'rework',
95
- }
96
- return next
97
- }
98
-
99
- export function recordReviewFailure(
100
- graph: TaskGraph,
101
- state: WorkflowState,
102
- taskHandle: string,
103
- reason: string,
104
- ): WorkflowState {
105
- const next = cloneState(state)
106
- const taskState = getTaskState(next, taskHandle)
107
- next.currentTaskHandle = null
108
- next.tasks[taskHandle] =
109
- taskState.attempt >= getMaxIterations(graph)
110
- ? {
111
- ...withReviewMetadata(taskState, {}),
112
- reason,
113
- status: 'blocked',
114
- }
115
- : {
116
- ...withReviewMetadata(taskState, {}),
117
- status: 'rework',
118
- }
119
- return next
120
- }
121
-
122
- export function buildReport(
123
- graph: TaskGraph,
124
- state: WorkflowState,
125
- generatedAt: string,
126
- ): FinalReport {
127
- const tasks = graph.tasks.map((task) => {
128
- const taskState = getTaskState(state, task.handle)
129
- return {
130
- attempt: taskState.attempt,
131
- generation: taskState.generation,
132
- taskHandle: task.handle,
133
- ...('commitSha' in taskState ? { commitSha: taskState.commitSha } : {}),
134
- ...(taskState.lastReviewVerdict
135
- ? { lastReviewVerdict: taskState.lastReviewVerdict }
136
- : {}),
137
- ...('reason' in taskState ? { reason: taskState.reason } : {}),
138
- status: taskState.status,
139
- }
140
- })
141
-
142
- const blockedTasks = tasks.filter((task) => task.status === 'blocked').length
143
- const completedTasks = tasks.filter((task) => task.status === 'done').length
144
- const replanTasks = tasks.filter((task) => task.status === 'replan').length
145
- const finalStatus =
146
- replanTasks > 0
147
- ? 'replan_required'
148
- : blockedTasks > 0
149
- ? 'blocked'
150
- : completedTasks === tasks.length
151
- ? 'completed'
152
- : 'in_progress'
153
-
154
- return {
155
- featureId: graph.featureId,
156
- generatedAt,
157
- tasks,
158
- summary: {
159
- blockedTasks,
160
- completedTasks,
161
- finalStatus,
162
- replanTasks,
163
- totalTasks: tasks.length,
164
- },
165
- }
166
- }
@@ -1,223 +0,0 @@
1
- import {
2
- canStartTask,
3
- cloneState,
4
- createBaseTaskState,
5
- getMaxIterations,
6
- getTaskState,
7
- shouldPassZeroGate,
8
- withReviewMetadata,
9
- } from './engine-helpers'
10
-
11
- import type { ReviewOutput, TaskGraph, WorkflowState } from '../types'
12
-
13
- export {
14
- buildReport,
15
- recordCommitFailure,
16
- recordIntegrateResult,
17
- recordReviewApproved,
18
- recordReviewFailure,
19
- } from './engine-outcomes'
20
-
21
- export function createInitialWorkflowState(graph: TaskGraph): WorkflowState {
22
- return {
23
- currentTaskHandle: null,
24
- featureId: graph.featureId,
25
- tasks: Object.fromEntries(
26
- graph.tasks.map((task) => [task.handle, createBaseTaskState()]),
27
- ),
28
- }
29
- }
30
-
31
- export function alignStateWithGraph(
32
- graph: TaskGraph,
33
- state: WorkflowState,
34
- options?: AlignStateWithGraphOptions,
35
- ): WorkflowState {
36
- const next = cloneState(state)
37
- const alignedTasks = Object.fromEntries(
38
- graph.tasks.map((task) => {
39
- const existing = next.tasks[task.handle] ?? createBaseTaskState()
40
- if (existing.status === 'running') {
41
- if (
42
- (options?.preserveRunningReview && existing.stage === 'review') ||
43
- (options?.preserveRunningIntegrate && existing.stage === 'integrate')
44
- ) {
45
- return [task.handle, existing]
46
- }
47
- return [
48
- task.handle,
49
- {
50
- attempt: existing.attempt,
51
- generation: existing.generation,
52
- invalidatedBy: existing.invalidatedBy,
53
- lastFindings: existing.lastFindings,
54
- ...(existing.lastReviewVerdict
55
- ? { lastReviewVerdict: existing.lastReviewVerdict }
56
- : {}),
57
- status: 'rework' as const,
58
- },
59
- ]
60
- }
61
- return [task.handle, existing]
62
- }),
63
- )
64
-
65
- return {
66
- featureId: graph.featureId,
67
- tasks: alignedTasks,
68
- currentTaskHandle:
69
- next.currentTaskHandle && alignedTasks[next.currentTaskHandle]
70
- ? next.currentTaskHandle
71
- : null,
72
- }
73
- }
74
-
75
- export function selectNextRunnableTask(
76
- graph: TaskGraph,
77
- state: WorkflowState,
78
- ): null | string {
79
- return (
80
- graph.tasks.find((task) => {
81
- const taskState = state.tasks[task.handle]
82
- if (!taskState) {
83
- return false
84
- }
85
- if (taskState.status === 'rework') {
86
- return true
87
- }
88
- return (
89
- taskState.status === 'pending' &&
90
- canStartTask(graph, state, task.handle)
91
- )
92
- })?.handle ?? null
93
- )
94
- }
95
-
96
- export function startAttempt(
97
- graph: TaskGraph,
98
- state: WorkflowState,
99
- taskHandle: string,
100
- ): WorkflowState {
101
- const taskState = getTaskState(state, taskHandle)
102
- if (taskState.status !== 'pending' && taskState.status !== 'rework') {
103
- throw new Error(`Task ${taskHandle} is not runnable`)
104
- }
105
- if (
106
- taskState.status === 'pending' &&
107
- !canStartTask(graph, state, taskHandle)
108
- ) {
109
- throw new Error(`Task ${taskHandle} dependencies are not completed`)
110
- }
111
- const next = cloneState(state)
112
- const current = getTaskState(next, taskHandle)
113
- next.currentTaskHandle = taskHandle
114
- next.tasks[taskHandle] = {
115
- ...current,
116
- attempt: current.attempt + 1,
117
- invalidatedBy: null,
118
- stage: 'implement',
119
- status: 'running',
120
- }
121
- return next
122
- }
123
-
124
- export function recordImplementSuccess(
125
- state: WorkflowState,
126
- taskHandle: string,
127
- ): WorkflowState {
128
- const next = cloneState(state)
129
- const taskState = getTaskState(next, taskHandle)
130
- if (taskState.status !== 'running' || taskState.stage !== 'implement') {
131
- throw new Error(`Task ${taskHandle} is not implementing`)
132
- }
133
- next.tasks[taskHandle] = {
134
- ...taskState,
135
- stage: 'review',
136
- status: 'running',
137
- }
138
- return next
139
- }
140
-
141
- export function recordImplementFailure(
142
- graph: TaskGraph,
143
- state: WorkflowState,
144
- taskHandle: string,
145
- reason: string,
146
- ): WorkflowState {
147
- const next = cloneState(state)
148
- const taskState = getTaskState(next, taskHandle)
149
- next.currentTaskHandle = null
150
- next.tasks[taskHandle] =
151
- taskState.attempt >= getMaxIterations(graph)
152
- ? {
153
- ...withReviewMetadata(taskState, {}),
154
- reason,
155
- status: 'blocked',
156
- }
157
- : {
158
- ...withReviewMetadata(taskState, {}),
159
- status: 'rework',
160
- }
161
- return next
162
- }
163
-
164
- export function recordReviewResult(
165
- graph: TaskGraph,
166
- state: WorkflowState,
167
- taskHandle: string,
168
- input: RecordReviewResultInput,
169
- ): WorkflowState {
170
- const next = cloneState(state)
171
- const taskState = getTaskState(next, taskHandle)
172
- next.currentTaskHandle = null
173
- const metadata = withReviewMetadata(taskState, {
174
- findings: input.review.findings,
175
- reviewVerdict: input.review.verdict,
176
- })
177
-
178
- if (shouldPassZeroGate(input)) {
179
- throw new Error(
180
- `Task ${taskHandle} requires integrate result after approved review`,
181
- )
182
- }
183
-
184
- if (input.review.verdict === 'replan') {
185
- next.tasks[taskHandle] = {
186
- ...metadata,
187
- reason: input.review.summary,
188
- status: 'replan',
189
- }
190
- return next
191
- }
192
-
193
- if (input.review.verdict === 'blocked') {
194
- next.tasks[taskHandle] = {
195
- ...metadata,
196
- reason: input.review.summary,
197
- status: 'blocked',
198
- }
199
- return next
200
- }
201
-
202
- next.tasks[taskHandle] =
203
- taskState.attempt >= getMaxIterations(graph)
204
- ? {
205
- ...metadata,
206
- reason: input.review.summary,
207
- status: 'blocked',
208
- }
209
- : {
210
- ...metadata,
211
- status: 'rework',
212
- }
213
- return next
214
- }
215
-
216
- export interface AlignStateWithGraphOptions {
217
- preserveRunningIntegrate?: boolean
218
- preserveRunningReview?: boolean
219
- }
220
-
221
- export interface RecordReviewResultInput {
222
- review: ReviewOutput
223
- }
@@ -1,52 +0,0 @@
1
- import { buildReport } from './engine'
2
-
3
- import type {
4
- ImplementArtifact,
5
- ReviewArtifact,
6
- TaskGraph,
7
- WorkflowEvent,
8
- WorkflowState,
9
- } from '../types'
10
- import type { OrchestratorRuntime } from './runtime'
11
-
12
- export function now() {
13
- return new Date().toISOString()
14
- }
15
-
16
- export async function persistState(
17
- runtime: OrchestratorRuntime,
18
- graph: TaskGraph,
19
- state: WorkflowState,
20
- ) {
21
- await runtime.store.saveState(state)
22
- const report = buildReport(graph, state, now())
23
- await runtime.store.saveReport(report)
24
- return report
25
- }
26
-
27
- export async function appendEvent(
28
- runtime: OrchestratorRuntime,
29
- event: WorkflowEvent,
30
- ) {
31
- await runtime.store.appendEvent(event)
32
- }
33
-
34
- export async function persistCommittedArtifacts(
35
- runtime: OrchestratorRuntime,
36
- input: PersistCommittedArtifactsInput,
37
- ) {
38
- await runtime.store.saveImplementArtifact({
39
- ...input.implementArtifact,
40
- commitSha: input.commitSha,
41
- })
42
- await runtime.store.saveReviewArtifact({
43
- ...input.reviewArtifact,
44
- commitSha: input.commitSha,
45
- })
46
- }
47
-
48
- export interface PersistCommittedArtifactsInput {
49
- commitSha: string
50
- implementArtifact: ImplementArtifact
51
- reviewArtifact: ReviewArtifact
52
- }
@@ -1,149 +0,0 @@
1
- import {
2
- isPullRequestWorkflowPreset,
3
- type WorkflowRuntime,
4
- } from '../workflow/preset'
5
- import { recordCommitFailure, recordIntegrateResult } from './engine'
6
- import {
7
- appendEvent,
8
- now,
9
- persistCommittedArtifacts,
10
- persistState,
11
- } from './orchestrator-helpers'
12
-
13
- import type {
14
- FinalReport,
15
- IntegrateArtifact,
16
- TaskGraph,
17
- WorkflowState,
18
- } from '../types'
19
- import type { OrchestratorRuntime } from './runtime'
20
-
21
- export interface ResumePullRequestIntegrateInput {
22
- graph: TaskGraph
23
- runtime: OrchestratorRuntime
24
- state: WorkflowState
25
- workflow: WorkflowRuntime
26
- }
27
-
28
- export interface ResumePullRequestIntegrateResult {
29
- report: FinalReport
30
- state: WorkflowState
31
- }
32
-
33
- export async function resumePullRequestIntegrate(
34
- input: ResumePullRequestIntegrateInput,
35
- ): Promise<null | ResumePullRequestIntegrateResult> {
36
- if (
37
- !isPullRequestWorkflowPreset(input.workflow.preset) ||
38
- !input.state.currentTaskHandle
39
- ) {
40
- return null
41
- }
42
-
43
- const taskHandle = input.state.currentTaskHandle
44
- const taskState = input.state.tasks[taskHandle]
45
- if (taskState?.status !== 'running' || taskState.stage !== 'integrate') {
46
- return null
47
- }
48
-
49
- const artifactKey = {
50
- attempt: taskState.attempt,
51
- generation: taskState.generation,
52
- taskHandle,
53
- }
54
- const [implementArtifact, reviewArtifact] = await Promise.all([
55
- input.runtime.store.loadImplementArtifact(artifactKey),
56
- input.runtime.store.loadReviewArtifact(artifactKey),
57
- ])
58
-
59
- if (!implementArtifact || !reviewArtifact) {
60
- const reason = `Cannot resume integrate for ${taskHandle} without persisted implement and review artifacts`
61
- const nextState = recordCommitFailure(
62
- input.graph,
63
- input.state,
64
- taskHandle,
65
- reason,
66
- )
67
- await appendEvent(input.runtime, {
68
- attempt: taskState.attempt,
69
- detail: reason,
70
- generation: taskState.generation,
71
- taskHandle,
72
- timestamp: now(),
73
- type: 'integrate_failed',
74
- })
75
- const report = await persistState(input.runtime, input.graph, nextState)
76
- return {
77
- report,
78
- state: nextState,
79
- }
80
- }
81
-
82
- const commitMessage = input.runtime.taskSource.buildCommitSubject(taskHandle)
83
-
84
- let integrateResult
85
- try {
86
- integrateResult = await input.workflow.preset.integrate({
87
- commitMessage,
88
- runtime: input.runtime,
89
- taskHandle,
90
- })
91
- } catch (error) {
92
- const reason = error instanceof Error ? error.message : String(error)
93
- const nextState = recordCommitFailure(
94
- input.graph,
95
- input.state,
96
- taskHandle,
97
- reason,
98
- )
99
- await appendEvent(input.runtime, {
100
- attempt: taskState.attempt,
101
- detail: reason,
102
- generation: taskState.generation,
103
- taskHandle,
104
- timestamp: now(),
105
- type: 'integrate_failed',
106
- })
107
- const report = await persistState(input.runtime, input.graph, nextState)
108
- return {
109
- report,
110
- state: nextState,
111
- }
112
- }
113
-
114
- const integrateArtifact: IntegrateArtifact = {
115
- attempt: taskState.attempt,
116
- createdAt: now(),
117
- generation: taskState.generation,
118
- result: integrateResult.result,
119
- taskHandle,
120
- }
121
- const nextState = recordIntegrateResult(
122
- input.graph,
123
- input.state,
124
- taskHandle,
125
- {
126
- commitSha: integrateResult.result.commitSha,
127
- review: reviewArtifact.result,
128
- },
129
- )
130
- const report = await persistState(input.runtime, input.graph, nextState)
131
- await appendEvent(input.runtime, {
132
- attempt: taskState.attempt,
133
- detail: integrateResult.result.summary,
134
- generation: taskState.generation,
135
- taskHandle,
136
- timestamp: now(),
137
- type: 'integrate_completed',
138
- })
139
- await input.runtime.store.saveIntegrateArtifact(integrateArtifact)
140
- await persistCommittedArtifacts(input.runtime, {
141
- commitSha: integrateResult.result.commitSha,
142
- implementArtifact,
143
- reviewArtifact,
144
- })
145
- return {
146
- report,
147
- state: nextState,
148
- }
149
- }