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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/bin/task-while.mjs +22 -0
  4. package/package.json +72 -0
  5. package/src/agents/claude.ts +175 -0
  6. package/src/agents/codex.ts +231 -0
  7. package/src/agents/provider-options.ts +45 -0
  8. package/src/agents/types.ts +69 -0
  9. package/src/batch/config.ts +109 -0
  10. package/src/batch/discovery.ts +35 -0
  11. package/src/batch/provider.ts +79 -0
  12. package/src/commands/batch.ts +266 -0
  13. package/src/commands/run.ts +270 -0
  14. package/src/core/engine-helpers.ts +114 -0
  15. package/src/core/engine-outcomes.ts +166 -0
  16. package/src/core/engine.ts +223 -0
  17. package/src/core/orchestrator-helpers.ts +52 -0
  18. package/src/core/orchestrator-integrate-resume.ts +149 -0
  19. package/src/core/orchestrator-review-resume.ts +228 -0
  20. package/src/core/orchestrator-task-attempt.ts +257 -0
  21. package/src/core/orchestrator.ts +99 -0
  22. package/src/core/runtime.ts +175 -0
  23. package/src/core/task-topology.ts +85 -0
  24. package/src/index.ts +121 -0
  25. package/src/prompts/implementer.ts +18 -0
  26. package/src/prompts/reviewer.ts +26 -0
  27. package/src/runtime/fs-runtime.ts +209 -0
  28. package/src/runtime/git.ts +137 -0
  29. package/src/runtime/github-pr-snapshot-decode.ts +307 -0
  30. package/src/runtime/github-pr-snapshot-queries.ts +137 -0
  31. package/src/runtime/github-pr-snapshot.ts +139 -0
  32. package/src/runtime/github.ts +232 -0
  33. package/src/runtime/path-layout.ts +13 -0
  34. package/src/runtime/workspace-resolver.ts +125 -0
  35. package/src/schema/index.ts +127 -0
  36. package/src/schema/model.ts +233 -0
  37. package/src/schema/shared.ts +93 -0
  38. package/src/task-sources/openspec/cli-json.ts +79 -0
  39. package/src/task-sources/openspec/context-files.ts +121 -0
  40. package/src/task-sources/openspec/parse-tasks-md.ts +57 -0
  41. package/src/task-sources/openspec/session.ts +235 -0
  42. package/src/task-sources/openspec/source.ts +59 -0
  43. package/src/task-sources/registry.ts +22 -0
  44. package/src/task-sources/spec-kit/parse-tasks-md.ts +48 -0
  45. package/src/task-sources/spec-kit/session.ts +174 -0
  46. package/src/task-sources/spec-kit/source.ts +30 -0
  47. package/src/task-sources/types.ts +47 -0
  48. package/src/types.ts +29 -0
  49. package/src/utils/fs.ts +31 -0
  50. package/src/workflow/config.ts +127 -0
  51. package/src/workflow/direct-preset.ts +44 -0
  52. package/src/workflow/finalize-task-checkbox.ts +24 -0
  53. package/src/workflow/preset.ts +86 -0
  54. package/src/workflow/pull-request-preset.ts +312 -0
  55. 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
+ }