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,231 @@
1
+ import { buildImplementerPrompt } from '../prompts/implementer'
2
+ import { buildReviewerPrompt } from '../prompts/reviewer'
3
+ import {
4
+ implementOutputSchema,
5
+ reviewOutputSchema,
6
+ validateImplementOutput,
7
+ validateReviewOutput,
8
+ } from '../schema/index'
9
+
10
+ import type { CodexProviderOptions } from './provider-options'
11
+ import type {
12
+ ImplementAgentInput,
13
+ ImplementerProvider,
14
+ ReviewAgentInput,
15
+ ReviewerProvider,
16
+ } from './types'
17
+
18
+ export interface CodexRunResult {
19
+ finalResponse: string
20
+ }
21
+
22
+ export interface CodexUsage {
23
+ cached_input_tokens: number
24
+ input_tokens: number
25
+ output_tokens: number
26
+ }
27
+
28
+ export interface CodexTurnFailedEvent {
29
+ error: CodexTurnFailedError
30
+ type: 'turn.failed'
31
+ }
32
+
33
+ export interface CodexItemEvent {
34
+ item: CodexItemPayload
35
+ type: 'item.completed' | 'item.started' | 'item.updated'
36
+ }
37
+
38
+ export interface CodexErrorEvent {
39
+ message: string
40
+ type: 'error'
41
+ }
42
+
43
+ export interface CodexThreadStartedEvent {
44
+ thread_id: string
45
+ type: 'thread.started'
46
+ }
47
+
48
+ export interface CodexTurnCompletedEvent {
49
+ type: 'turn.completed'
50
+ usage: CodexUsage
51
+ }
52
+
53
+ export interface CodexTurnStartedEvent {
54
+ type: 'turn.started'
55
+ }
56
+
57
+ export interface CodexTurnFailedError {
58
+ message: string
59
+ }
60
+
61
+ export interface CodexItemPayload {
62
+ text?: string
63
+ type: string
64
+ }
65
+
66
+ export type CodexThreadEvent =
67
+ | CodexErrorEvent
68
+ | CodexItemEvent
69
+ | CodexThreadStartedEvent
70
+ | CodexTurnCompletedEvent
71
+ | CodexTurnFailedEvent
72
+ | CodexTurnStartedEvent
73
+
74
+ export type CodexThreadEventHandler = (event: CodexThreadEvent) => void
75
+
76
+ export interface CodexRunStreamedResult {
77
+ events: AsyncGenerator<CodexThreadEvent>
78
+ }
79
+
80
+ export interface CodexThreadRunOptions {
81
+ outputSchema: Record<string, unknown>
82
+ }
83
+
84
+ export interface CodexThreadLike {
85
+ run: (
86
+ prompt: string,
87
+ options: CodexThreadRunOptions,
88
+ ) => Promise<CodexRunResult>
89
+ runStreamed?: (
90
+ prompt: string,
91
+ options: CodexThreadRunOptions,
92
+ ) => Promise<CodexRunStreamedResult>
93
+ }
94
+
95
+ export interface CodexStartThreadOptions {
96
+ model?: string
97
+ modelReasoningEffort?: 'high' | 'low' | 'medium' | 'minimal' | 'xhigh'
98
+ workingDirectory: string
99
+ }
100
+
101
+ export interface CodexClientLike {
102
+ startThread: (options: CodexStartThreadOptions) => CodexThreadLike
103
+ }
104
+
105
+ export interface CodexAgentClientOptions extends CodexProviderOptions {
106
+ onEvent?: CodexThreadEventHandler
107
+ workspaceRoot: string
108
+ }
109
+
110
+ export interface CodexStructuredInput {
111
+ outputSchema: Record<string, unknown>
112
+ prompt: string
113
+ }
114
+
115
+ async function defaultClientFactory(): Promise<CodexClientLike> {
116
+ const { Codex } = await import('@openai/codex-sdk')
117
+ return new Codex()
118
+ }
119
+
120
+ export class CodexAgentClient implements ImplementerProvider, ReviewerProvider {
121
+ private clientPromise: null | Promise<CodexClientLike> = null
122
+ public readonly name = 'codex'
123
+
124
+ public constructor(private readonly options: CodexAgentClientOptions) {}
125
+
126
+ private async collectStreamedTurn<T>(
127
+ thread: CodexThreadLike,
128
+ input: CodexStructuredInput,
129
+ ): Promise<T> {
130
+ const streamedTurn = await thread.runStreamed!(input.prompt, {
131
+ outputSchema: input.outputSchema,
132
+ })
133
+ let finalResponse = ''
134
+
135
+ for await (const event of streamedTurn.events) {
136
+ this.options.onEvent?.(event)
137
+
138
+ if (event.type === 'error') {
139
+ throw new Error(event.message)
140
+ }
141
+
142
+ if (event.type === 'turn.failed') {
143
+ throw new Error(event.error.message)
144
+ }
145
+
146
+ if (
147
+ event.type === 'item.completed' &&
148
+ event.item.type === 'agent_message'
149
+ ) {
150
+ finalResponse = event.item.text?.trim() ?? ''
151
+ }
152
+ }
153
+
154
+ if (!finalResponse) {
155
+ throw new Error('Codex agent client returned empty finalResponse')
156
+ }
157
+
158
+ try {
159
+ return JSON.parse(finalResponse) as T
160
+ } catch (error) {
161
+ throw new Error('Codex agent client returned non-JSON finalResponse', {
162
+ cause: error,
163
+ })
164
+ }
165
+ }
166
+
167
+ private async getClient(): Promise<CodexClientLike> {
168
+ this.clientPromise ??= defaultClientFactory()
169
+ return this.clientPromise
170
+ }
171
+
172
+ public async implement(input: ImplementAgentInput) {
173
+ const prompt = await buildImplementerPrompt(input)
174
+ const output = await this.invokeStructured<unknown>({
175
+ outputSchema: implementOutputSchema,
176
+ prompt,
177
+ })
178
+ return validateImplementOutput(output)
179
+ }
180
+
181
+ public async invokeStructured<T>(input: CodexStructuredInput): Promise<T> {
182
+ const client = await this.getClient()
183
+ const startThreadOptions: CodexStartThreadOptions = {
184
+ workingDirectory: this.options.workspaceRoot,
185
+ }
186
+
187
+ if (this.options.model) {
188
+ startThreadOptions.model = this.options.model
189
+ }
190
+
191
+ if (this.options.effort) {
192
+ startThreadOptions.modelReasoningEffort = this.options.effort
193
+ }
194
+
195
+ const thread = client.startThread(startThreadOptions)
196
+
197
+ if (this.options.onEvent && thread.runStreamed) {
198
+ return this.collectStreamedTurn<T>(thread, input)
199
+ }
200
+
201
+ const turn = await thread.run(input.prompt, {
202
+ outputSchema: input.outputSchema,
203
+ })
204
+ const response = turn.finalResponse.trim()
205
+ if (!response) {
206
+ throw new Error('Codex agent client returned empty finalResponse')
207
+ }
208
+ try {
209
+ return JSON.parse(response) as T
210
+ } catch (error) {
211
+ throw new Error('Codex agent client returned non-JSON finalResponse', {
212
+ cause: error,
213
+ })
214
+ }
215
+ }
216
+
217
+ public async review(input: ReviewAgentInput) {
218
+ const prompt = await buildReviewerPrompt(input)
219
+ const output = await this.invokeStructured<unknown>({
220
+ outputSchema: reviewOutputSchema,
221
+ prompt,
222
+ })
223
+ return validateReviewOutput(output)
224
+ }
225
+ }
226
+
227
+ export function createCodexProvider(
228
+ options: CodexAgentClientOptions,
229
+ ): ImplementerProvider & ReviewerProvider {
230
+ return new CodexAgentClient(options)
231
+ }
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod'
2
+
3
+ const modelSchema = z.string().trim().min(1)
4
+
5
+ export const codexEffortSchema = z.enum([
6
+ 'minimal',
7
+ 'low',
8
+ 'medium',
9
+ 'high',
10
+ 'xhigh',
11
+ ])
12
+
13
+ export const claudeEffortSchema = z.enum(['low', 'medium', 'high', 'max'])
14
+
15
+ export const codexProviderOptionsSchema = z
16
+ .object({
17
+ effort: codexEffortSchema.optional(),
18
+ model: modelSchema.optional(),
19
+ })
20
+ .strict()
21
+
22
+ export const claudeProviderOptionsSchema = z
23
+ .object({
24
+ effort: claudeEffortSchema.optional(),
25
+ model: modelSchema.optional(),
26
+ })
27
+ .strict()
28
+
29
+ export type CodexProviderOptions = z.infer<typeof codexProviderOptionsSchema>
30
+ export type ClaudeProviderOptions = z.infer<typeof claudeProviderOptionsSchema>
31
+
32
+ export type WorkflowRoleProviderOptions =
33
+ | (ClaudeProviderOptions & { provider: 'claude' })
34
+ | (CodexProviderOptions & { provider: 'codex' })
35
+
36
+ export function providerOptionsEqual(
37
+ left: WorkflowRoleProviderOptions,
38
+ right: WorkflowRoleProviderOptions,
39
+ ) {
40
+ return (
41
+ left.provider === right.provider &&
42
+ left.model === right.model &&
43
+ left.effort === right.effort
44
+ )
45
+ }
@@ -0,0 +1,69 @@
1
+ import type { PullRequestSnapshot } from '../core/runtime'
2
+ import type { TaskPrompt } from '../task-sources/types'
3
+ import type { ImplementOutput, ReviewFinding, ReviewOutput } from '../types'
4
+
5
+ export interface ImplementAgentInput {
6
+ attempt: number
7
+ generation: number
8
+ lastFindings: ReviewFinding[]
9
+ prompt: TaskPrompt
10
+ taskHandle: string
11
+ }
12
+
13
+ export interface ReviewAgentInput {
14
+ actualChangedFiles: string[]
15
+ attempt: number
16
+ generation: number
17
+ implement: ImplementOutput
18
+ lastFindings: ReviewFinding[]
19
+ prompt: TaskPrompt
20
+ taskHandle: string
21
+ }
22
+
23
+ export interface ImplementerProvider {
24
+ implement: (input: ImplementAgentInput) => Promise<ImplementOutput>
25
+ readonly name: string
26
+ }
27
+
28
+ export interface ReviewerProvider {
29
+ readonly name: string
30
+ review: (input: ReviewAgentInput) => Promise<ReviewOutput>
31
+ }
32
+
33
+ export interface PullRequestReviewInput {
34
+ checkpointStartedAt: string
35
+ completionCriteria: string[]
36
+ pullRequest: PullRequestSnapshot
37
+ taskHandle: string
38
+ }
39
+
40
+ export interface PullRequestReviewApprovedResult {
41
+ kind: 'approved'
42
+ review: ReviewOutput
43
+ }
44
+
45
+ export interface PullRequestReviewPendingResult {
46
+ kind: 'pending'
47
+ }
48
+
49
+ export interface PullRequestReviewRejectedResult {
50
+ kind: 'rejected'
51
+ review: ReviewOutput
52
+ }
53
+
54
+ export type PullRequestReviewResult =
55
+ | PullRequestReviewApprovedResult
56
+ | PullRequestReviewPendingResult
57
+ | PullRequestReviewRejectedResult
58
+
59
+ export interface RemoteReviewerProvider {
60
+ evaluatePullRequestReview: (
61
+ input: PullRequestReviewInput,
62
+ ) => Promise<PullRequestReviewResult>
63
+ readonly name: string
64
+ }
65
+
66
+ export interface WorkflowRoleProviders {
67
+ implementer: ImplementerProvider
68
+ reviewer: RemoteReviewerProvider | ReviewerProvider
69
+ }
@@ -0,0 +1,109 @@
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
+ export const batchProviderSchema = z.enum(['claude', 'codex'])
14
+
15
+ const jsonSchemaSchema = z.custom<Record<string, unknown>>(
16
+ (value) =>
17
+ typeof value === 'object' && value !== null && !Array.isArray(value),
18
+ {
19
+ message: 'schema must be an object',
20
+ },
21
+ )
22
+
23
+ const globEntrySchema = z.string().trim().min(1)
24
+
25
+ const globSchema = z
26
+ .union([globEntrySchema, z.array(globEntrySchema).min(1)])
27
+ .optional()
28
+
29
+ const commonBatchConfigSchema = z
30
+ .object({
31
+ glob: globSchema,
32
+ prompt: z.string().trim().min(1),
33
+ schema: jsonSchemaSchema,
34
+ })
35
+ .strict()
36
+
37
+ const batchConfigSchema = z.discriminatedUnion('provider', [
38
+ z
39
+ .object({
40
+ provider: z.literal('claude'),
41
+ })
42
+ .extend(claudeProviderOptionsSchema.shape)
43
+ .extend(commonBatchConfigSchema.shape)
44
+ .strict(),
45
+ z
46
+ .object({
47
+ provider: z.literal('codex'),
48
+ })
49
+ .extend(codexProviderOptionsSchema.shape)
50
+ .extend(commonBatchConfigSchema.shape)
51
+ .strict(),
52
+ ])
53
+
54
+ export type BatchProviderName = WorkflowRoleProviderOptions['provider']
55
+
56
+ interface BatchConfigBase {
57
+ configDir: string
58
+ configPath: string
59
+ glob: string[]
60
+ prompt: string
61
+ schema: Record<string, unknown>
62
+ }
63
+
64
+ export type BatchConfig = BatchConfigBase & WorkflowRoleProviderOptions
65
+
66
+ export interface LoadBatchConfigInput {
67
+ configPath: string
68
+ cwd: string
69
+ }
70
+
71
+ function normalizeGlobConfig(glob: string | string[] | undefined) {
72
+ if (!glob) {
73
+ return ['**/*']
74
+ }
75
+ return typeof glob === 'string' ? [glob] : glob
76
+ }
77
+
78
+ export async function loadBatchConfig(
79
+ input: LoadBatchConfigInput,
80
+ ): Promise<BatchConfig> {
81
+ const configPath = path.resolve(input.cwd, input.configPath)
82
+ const configSource = await readFile(configPath, 'utf8')
83
+ const rawConfig = parse(configSource) ?? {}
84
+ const parsedConfig = batchConfigSchema.parse(rawConfig)
85
+ const configDir = path.dirname(configPath)
86
+ const baseConfig = {
87
+ configDir,
88
+ configPath,
89
+ glob: normalizeGlobConfig(parsedConfig.glob),
90
+ prompt: parsedConfig.prompt,
91
+ schema: parsedConfig.schema,
92
+ }
93
+
94
+ if (parsedConfig.provider === 'claude') {
95
+ return {
96
+ ...baseConfig,
97
+ provider: 'claude',
98
+ ...(parsedConfig.model ? { model: parsedConfig.model } : {}),
99
+ ...(parsedConfig.effort ? { effort: parsedConfig.effort } : {}),
100
+ }
101
+ }
102
+
103
+ return {
104
+ ...baseConfig,
105
+ provider: 'codex',
106
+ ...(parsedConfig.model ? { model: parsedConfig.model } : {}),
107
+ ...(parsedConfig.effort ? { effort: parsedConfig.effort } : {}),
108
+ }
109
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'node:path'
2
+
3
+ import { glob } from 'glob'
4
+
5
+ export interface DiscoverBatchFilesInput {
6
+ baseDir: string
7
+ excludedFiles: Set<string>
8
+ patterns: string[]
9
+ }
10
+
11
+ function normalizeBatchPath(value: string) {
12
+ return value.split(path.sep).join('/')
13
+ }
14
+
15
+ export async function discoverBatchFiles(
16
+ input: DiscoverBatchFilesInput,
17
+ ): Promise<string[]> {
18
+ const excluded = new Set(
19
+ [...input.excludedFiles].map((filePath) =>
20
+ normalizeBatchPath(path.relative(input.baseDir, filePath)),
21
+ ),
22
+ )
23
+ const matched = await glob(input.patterns, {
24
+ absolute: false,
25
+ cwd: input.baseDir,
26
+ dot: true,
27
+ ignore: ['.git/**', 'node_modules/**'],
28
+ nodir: true,
29
+ posix: true,
30
+ })
31
+
32
+ return [...new Set(matched.map(normalizeBatchPath))]
33
+ .filter((filePath) => !excluded.has(filePath))
34
+ .sort((left, right) => left.localeCompare(right))
35
+ }
@@ -0,0 +1,79 @@
1
+ import { ClaudeAgentClient } from '../agents/claude'
2
+ import { CodexAgentClient } from '../agents/codex'
3
+
4
+ import type { WorkflowRoleProviderOptions } from '../agents/provider-options'
5
+
6
+ export interface BatchFileInput {
7
+ content: string
8
+ filePath: string
9
+ outputSchema: Record<string, unknown>
10
+ prompt: string
11
+ }
12
+
13
+ export interface BatchStructuredOutputProvider {
14
+ readonly name: string
15
+ runFile: (input: BatchFileInput) => Promise<unknown>
16
+ }
17
+
18
+ export type CreateBatchStructuredOutputProviderInput =
19
+ WorkflowRoleProviderOptions & {
20
+ workspaceRoot: string
21
+ }
22
+
23
+ function buildBatchPrompt(input: BatchFileInput) {
24
+ return [
25
+ 'Process exactly one file and return structured output only.',
26
+ input.prompt,
27
+ `File path: ${input.filePath}`,
28
+ 'File content:',
29
+ input.content,
30
+ ].join('\n\n')
31
+ }
32
+
33
+ class CodexBatchStructuredOutputProvider implements BatchStructuredOutputProvider {
34
+ public readonly name = 'codex'
35
+
36
+ public constructor(private readonly client: CodexAgentClient) {}
37
+
38
+ public async runFile(input: BatchFileInput) {
39
+ return this.client.invokeStructured<unknown>({
40
+ outputSchema: input.outputSchema,
41
+ prompt: buildBatchPrompt(input),
42
+ })
43
+ }
44
+ }
45
+
46
+ class ClaudeBatchStructuredOutputProvider implements BatchStructuredOutputProvider {
47
+ public readonly name = 'claude'
48
+
49
+ public constructor(private readonly client: ClaudeAgentClient) {}
50
+
51
+ public async runFile(input: BatchFileInput) {
52
+ return this.client.invokeStructured<unknown>({
53
+ outputSchema: input.outputSchema,
54
+ prompt: buildBatchPrompt(input),
55
+ })
56
+ }
57
+ }
58
+
59
+ export function createBatchStructuredOutputProvider(
60
+ input: CreateBatchStructuredOutputProviderInput,
61
+ ): BatchStructuredOutputProvider {
62
+ if (input.provider === 'codex') {
63
+ return new CodexBatchStructuredOutputProvider(
64
+ new CodexAgentClient({
65
+ ...(input.effort ? { effort: input.effort } : {}),
66
+ ...(input.model ? { model: input.model } : {}),
67
+ workspaceRoot: input.workspaceRoot,
68
+ }),
69
+ )
70
+ }
71
+
72
+ return new ClaudeBatchStructuredOutputProvider(
73
+ new ClaudeAgentClient({
74
+ ...(input.effort ? { effort: input.effort } : {}),
75
+ ...(input.model ? { model: input.model } : {}),
76
+ workspaceRoot: input.workspaceRoot,
77
+ }),
78
+ )
79
+ }