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,243 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestReviewInput,
|
|
3
|
+
PullRequestReviewResult,
|
|
4
|
+
RemoteReviewerProvider,
|
|
5
|
+
} from '../agents/types'
|
|
6
|
+
import type {
|
|
7
|
+
PullRequestDiscussionComment,
|
|
8
|
+
PullRequestReviewSummary,
|
|
9
|
+
PullRequestReviewThread,
|
|
10
|
+
PullRequestReviewThreadComment,
|
|
11
|
+
} from '../core/runtime'
|
|
12
|
+
import type { ReviewFinding, ReviewOutput } from '../types'
|
|
13
|
+
|
|
14
|
+
export const CODEX_REVIEWER_ACTOR = 'chatgpt-codex-connector[bot]'
|
|
15
|
+
|
|
16
|
+
export interface DiscussionFeedbackSignal extends PullRequestDiscussionComment {
|
|
17
|
+
kind: 'discussion'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ReviewSummaryFeedbackSignal extends PullRequestReviewSummary {
|
|
21
|
+
kind: 'review-summary'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ThreadFeedbackSignal extends PullRequestReviewThreadComment {
|
|
25
|
+
kind: 'thread'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type FeedbackSignal =
|
|
29
|
+
| DiscussionFeedbackSignal
|
|
30
|
+
| ReviewSummaryFeedbackSignal
|
|
31
|
+
| ThreadFeedbackSignal
|
|
32
|
+
|
|
33
|
+
function isNonNull<T>(value: null | T): value is T {
|
|
34
|
+
return value !== null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isAfterCheckpoint(
|
|
38
|
+
timestamp: null | string,
|
|
39
|
+
checkpointStartedAt: string,
|
|
40
|
+
) {
|
|
41
|
+
if (typeof timestamp !== 'string') {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
const eventAt = Date.parse(timestamp)
|
|
45
|
+
const checkpointAt = Date.parse(checkpointStartedAt)
|
|
46
|
+
return Number.isFinite(eventAt) && Number.isFinite(checkpointAt)
|
|
47
|
+
? eventAt >= checkpointAt
|
|
48
|
+
: false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compareTimestamps(left: string, right: string) {
|
|
52
|
+
return Date.parse(left) - Date.parse(right)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isAtOrAfter(left: string, right: string) {
|
|
56
|
+
return compareTimestamps(left, right) >= 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isActor(login: string) {
|
|
60
|
+
return login === CODEX_REVIEWER_ACTOR
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function collectThreadFeedback(
|
|
64
|
+
thread: PullRequestReviewThread,
|
|
65
|
+
checkpointStartedAt: string,
|
|
66
|
+
) {
|
|
67
|
+
if (thread.isResolved || thread.isOutdated) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
const latest = [...thread.comments]
|
|
71
|
+
.filter((comment) => isActor(comment.userLogin))
|
|
72
|
+
.sort((left, right) => compareTimestamps(left.createdAt, right.createdAt))
|
|
73
|
+
.at(-1)
|
|
74
|
+
if (!latest || !isAfterCheckpoint(latest.createdAt, checkpointStartedAt)) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
...latest,
|
|
79
|
+
kind: 'thread' as const,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function collectFeedbackSignals(
|
|
84
|
+
input: PullRequestReviewInput,
|
|
85
|
+
): FeedbackSignal[] {
|
|
86
|
+
const discussion = input.pullRequest.discussionComments
|
|
87
|
+
.filter(
|
|
88
|
+
(comment) =>
|
|
89
|
+
isActor(comment.userLogin) &&
|
|
90
|
+
isAfterCheckpoint(comment.createdAt, input.checkpointStartedAt),
|
|
91
|
+
)
|
|
92
|
+
.map((comment) => ({
|
|
93
|
+
...comment,
|
|
94
|
+
kind: 'discussion' as const,
|
|
95
|
+
}))
|
|
96
|
+
const reviewSummaries = input.pullRequest.reviewSummaries
|
|
97
|
+
.filter(
|
|
98
|
+
(summary) =>
|
|
99
|
+
isActor(summary.userLogin) &&
|
|
100
|
+
isAfterCheckpoint(summary.submittedAt, input.checkpointStartedAt),
|
|
101
|
+
)
|
|
102
|
+
.map((summary) => ({
|
|
103
|
+
...summary,
|
|
104
|
+
kind: 'review-summary' as const,
|
|
105
|
+
}))
|
|
106
|
+
const threads = input.pullRequest.reviewThreads
|
|
107
|
+
.map((thread) => collectThreadFeedback(thread, input.checkpointStartedAt))
|
|
108
|
+
.filter(isNonNull)
|
|
109
|
+
|
|
110
|
+
return [...discussion, ...reviewSummaries, ...threads].sort((left, right) => {
|
|
111
|
+
const leftTime =
|
|
112
|
+
'submittedAt' in left ? (left.submittedAt ?? '') : left.createdAt
|
|
113
|
+
const rightTime =
|
|
114
|
+
'submittedAt' in right ? (right.submittedAt ?? '') : right.createdAt
|
|
115
|
+
return compareTimestamps(leftTime, rightTime)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getFeedbackTimestamp(signal: FeedbackSignal) {
|
|
120
|
+
return 'submittedAt' in signal ? (signal.submittedAt ?? '') : signal.createdAt
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function latestApprovalTimestamp(input: PullRequestReviewInput) {
|
|
124
|
+
return (
|
|
125
|
+
input.pullRequest.reactions
|
|
126
|
+
.filter(
|
|
127
|
+
(reaction) =>
|
|
128
|
+
reaction.content === '+1' &&
|
|
129
|
+
isActor(reaction.userLogin) &&
|
|
130
|
+
isAfterCheckpoint(reaction.createdAt, input.checkpointStartedAt),
|
|
131
|
+
)
|
|
132
|
+
.map((reaction) => reaction.createdAt)
|
|
133
|
+
.sort(compareTimestamps)
|
|
134
|
+
.at(-1) ?? null
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function signalSeverity(signal: FeedbackSignal): 'high' | 'medium' {
|
|
139
|
+
if (
|
|
140
|
+
signal.kind === 'review-summary' &&
|
|
141
|
+
signal.state === 'CHANGES_REQUESTED'
|
|
142
|
+
) {
|
|
143
|
+
return 'high'
|
|
144
|
+
}
|
|
145
|
+
return 'medium'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizedCompletionCriteria(input: PullRequestReviewInput) {
|
|
149
|
+
if (input.completionCriteria.length !== 0) {
|
|
150
|
+
return input.completionCriteria
|
|
151
|
+
}
|
|
152
|
+
return [`Task ${input.taskHandle} matches the current task requirements`]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildApprovedReview(input: PullRequestReviewInput): ReviewOutput {
|
|
156
|
+
const completionCriteria = normalizedCompletionCriteria(input)
|
|
157
|
+
return {
|
|
158
|
+
findings: [],
|
|
159
|
+
overallRisk: 'low',
|
|
160
|
+
summary: `Remote reviewer ${CODEX_REVIEWER_ACTOR} approved the pull request`,
|
|
161
|
+
taskHandle: input.taskHandle,
|
|
162
|
+
verdict: 'pass',
|
|
163
|
+
acceptanceChecks: completionCriteria.map((criterion) => ({
|
|
164
|
+
criterion,
|
|
165
|
+
note: `Remote reviewer ${CODEX_REVIEWER_ACTOR} approved the pull request`,
|
|
166
|
+
status: 'pass' as const,
|
|
167
|
+
})),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildRejectedReview(
|
|
172
|
+
input: PullRequestReviewInput,
|
|
173
|
+
feedbackSignals: FeedbackSignal[],
|
|
174
|
+
): ReviewOutput {
|
|
175
|
+
const completionCriteria = normalizedCompletionCriteria(input)
|
|
176
|
+
const findings: ReviewFinding[] = feedbackSignals.map((signal) => {
|
|
177
|
+
const path = 'path' in signal && signal.path ? signal.path : undefined
|
|
178
|
+
const issue = signal.body.trim() || 'Remote reviewer requested changes'
|
|
179
|
+
return {
|
|
180
|
+
...(path ? { file: path } : {}),
|
|
181
|
+
fixHint: issue,
|
|
182
|
+
issue,
|
|
183
|
+
severity: signalSeverity(signal),
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
return {
|
|
187
|
+
findings,
|
|
188
|
+
taskHandle: input.taskHandle,
|
|
189
|
+
verdict: 'rework',
|
|
190
|
+
acceptanceChecks: completionCriteria.map((criterion) => ({
|
|
191
|
+
criterion,
|
|
192
|
+
note: 'Remote review left active feedback',
|
|
193
|
+
status: 'unclear' as const,
|
|
194
|
+
})),
|
|
195
|
+
overallRisk: findings.some((finding) => finding.severity === 'high')
|
|
196
|
+
? 'high'
|
|
197
|
+
: 'medium',
|
|
198
|
+
summary:
|
|
199
|
+
feedbackSignals
|
|
200
|
+
.map((signal) => signal.body.trim())
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.join('\n') || 'Remote reviewer left active feedback',
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createCodexRemoteReviewerProvider(): RemoteReviewerProvider {
|
|
207
|
+
return {
|
|
208
|
+
name: 'codex',
|
|
209
|
+
async evaluatePullRequestReview(
|
|
210
|
+
input: PullRequestReviewInput,
|
|
211
|
+
): Promise<PullRequestReviewResult> {
|
|
212
|
+
const feedbackSignals = collectFeedbackSignals(input)
|
|
213
|
+
const latestFeedbackTimestamp =
|
|
214
|
+
feedbackSignals
|
|
215
|
+
.map((signal) => getFeedbackTimestamp(signal))
|
|
216
|
+
.sort(compareTimestamps)
|
|
217
|
+
.at(-1) ?? null
|
|
218
|
+
const approvalTimestamp = latestApprovalTimestamp(input)
|
|
219
|
+
|
|
220
|
+
if (
|
|
221
|
+
approvalTimestamp &&
|
|
222
|
+
(!latestFeedbackTimestamp ||
|
|
223
|
+
isAtOrAfter(approvalTimestamp, latestFeedbackTimestamp))
|
|
224
|
+
) {
|
|
225
|
+
return {
|
|
226
|
+
kind: 'approved',
|
|
227
|
+
review: buildApprovedReview(input),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (feedbackSignals.length !== 0) {
|
|
232
|
+
return {
|
|
233
|
+
kind: 'rejected',
|
|
234
|
+
review: buildRejectedReview(input, feedbackSignals),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
kind: 'pending',
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
}
|