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,233 @@
1
+ import { z } from 'zod'
2
+
3
+ import {
4
+ acceptanceStatusValues,
5
+ dateTimeSchema,
6
+ finalStatusValues,
7
+ findingSeverityValues,
8
+ implementStatusValues,
9
+ nonEmptyStringSchema,
10
+ overallRiskValues,
11
+ reviewVerdictValues,
12
+ runningStageValues,
13
+ taskHandleSchema,
14
+ taskStatusValues,
15
+ uniqueStringArray,
16
+ workflowEventTypeValues,
17
+ } from './shared'
18
+
19
+ export const reviewFindingSchema = z
20
+ .object({
21
+ file: nonEmptyStringSchema.optional(),
22
+ fixHint: nonEmptyStringSchema,
23
+ issue: nonEmptyStringSchema,
24
+ severity: z.enum(findingSeverityValues),
25
+ })
26
+ .strict()
27
+
28
+ export const acceptanceCheckSchema = z
29
+ .object({
30
+ criterion: nonEmptyStringSchema,
31
+ note: nonEmptyStringSchema,
32
+ status: z.enum(acceptanceStatusValues),
33
+ })
34
+ .strict()
35
+
36
+ export const taskTopologyEntrySchema = z
37
+ .object({
38
+ commitSubject: nonEmptyStringSchema,
39
+ dependsOn: uniqueStringArray('dependency task handle'),
40
+ handle: taskHandleSchema,
41
+ })
42
+ .strict()
43
+
44
+ export const taskGraphSchema = z
45
+ .object({
46
+ featureId: nonEmptyStringSchema,
47
+ maxIterations: z.number().int().min(1).max(20),
48
+ tasks: z
49
+ .array(taskTopologyEntrySchema)
50
+ .min(1)
51
+ .superRefine((tasks, ctx) => {
52
+ const seen = new Set<string>()
53
+ for (const [index, task] of tasks.entries()) {
54
+ if (seen.has(task.handle)) {
55
+ ctx.addIssue({
56
+ code: z.ZodIssueCode.custom,
57
+ message: `Duplicate task handle: ${task.handle}`,
58
+ path: [index, 'handle'],
59
+ })
60
+ continue
61
+ }
62
+ seen.add(task.handle)
63
+ }
64
+ }),
65
+ })
66
+ .strict()
67
+
68
+ export const implementOutputSchemaInternal = z
69
+ .object({
70
+ assumptions: uniqueStringArray('assumption'),
71
+ needsHumanAttention: z.boolean(),
72
+ notes: uniqueStringArray('note'),
73
+ status: z.enum(implementStatusValues),
74
+ summary: nonEmptyStringSchema,
75
+ taskHandle: taskHandleSchema,
76
+ unresolvedItems: uniqueStringArray('unresolved item'),
77
+ })
78
+ .strict()
79
+
80
+ export const reviewOutputSchemaInternal = z
81
+ .object({
82
+ acceptanceChecks: z.array(acceptanceCheckSchema).min(1),
83
+ findings: z.array(reviewFindingSchema),
84
+ overallRisk: z.enum(overallRiskValues),
85
+ summary: nonEmptyStringSchema,
86
+ taskHandle: taskHandleSchema,
87
+ verdict: z.enum(reviewVerdictValues),
88
+ })
89
+ .strict()
90
+
91
+ const taskStateBaseSchema = z
92
+ .object({
93
+ attempt: z.number().int().min(0),
94
+ generation: z.number().int().min(1),
95
+ invalidatedBy: taskHandleSchema.nullable(),
96
+ lastFindings: z.array(reviewFindingSchema),
97
+ lastReviewVerdict: z.enum(reviewVerdictValues).optional(),
98
+ })
99
+ .strict()
100
+
101
+ export const pendingTaskStateSchema = taskStateBaseSchema
102
+ .extend({
103
+ status: z.literal('pending'),
104
+ })
105
+ .strict()
106
+
107
+ export const runningTaskStateSchema = taskStateBaseSchema
108
+ .extend({
109
+ stage: z.enum(runningStageValues),
110
+ status: z.literal('running'),
111
+ })
112
+ .strict()
113
+
114
+ export const reworkTaskStateSchema = taskStateBaseSchema
115
+ .extend({
116
+ status: z.literal('rework'),
117
+ })
118
+ .strict()
119
+
120
+ export const doneTaskStateSchema = taskStateBaseSchema
121
+ .extend({
122
+ commitSha: nonEmptyStringSchema,
123
+ status: z.literal('done'),
124
+ })
125
+ .strict()
126
+
127
+ export const blockedTaskStateSchema = taskStateBaseSchema
128
+ .extend({
129
+ reason: nonEmptyStringSchema,
130
+ status: z.literal('blocked'),
131
+ })
132
+ .strict()
133
+
134
+ export const replanTaskStateSchema = taskStateBaseSchema
135
+ .extend({
136
+ reason: nonEmptyStringSchema,
137
+ status: z.literal('replan'),
138
+ })
139
+ .strict()
140
+
141
+ export const taskStateSchema = z.discriminatedUnion('status', [
142
+ pendingTaskStateSchema,
143
+ runningTaskStateSchema,
144
+ reworkTaskStateSchema,
145
+ doneTaskStateSchema,
146
+ blockedTaskStateSchema,
147
+ replanTaskStateSchema,
148
+ ])
149
+
150
+ export const workflowStateSchema = z
151
+ .object({
152
+ currentTaskHandle: taskHandleSchema.nullable(),
153
+ featureId: nonEmptyStringSchema,
154
+ tasks: z.record(taskStateSchema),
155
+ })
156
+ .strict()
157
+
158
+ export const implementArtifactSchema = z
159
+ .object({
160
+ attempt: z.number().int().min(1),
161
+ commitSha: nonEmptyStringSchema.optional(),
162
+ createdAt: dateTimeSchema,
163
+ generation: z.number().int().min(1),
164
+ result: implementOutputSchemaInternal,
165
+ taskHandle: taskHandleSchema,
166
+ })
167
+ .strict()
168
+
169
+ export const reviewArtifactSchema = z
170
+ .object({
171
+ attempt: z.number().int().min(1),
172
+ commitSha: nonEmptyStringSchema.optional(),
173
+ createdAt: dateTimeSchema,
174
+ generation: z.number().int().min(1),
175
+ result: reviewOutputSchemaInternal,
176
+ taskHandle: taskHandleSchema,
177
+ })
178
+ .strict()
179
+
180
+ export const integrateArtifactSchema = z
181
+ .object({
182
+ attempt: z.number().int().min(1),
183
+ createdAt: dateTimeSchema,
184
+ generation: z.number().int().min(1),
185
+ taskHandle: taskHandleSchema,
186
+ result: z
187
+ .object({
188
+ commitSha: nonEmptyStringSchema,
189
+ summary: nonEmptyStringSchema,
190
+ })
191
+ .strict(),
192
+ })
193
+ .strict()
194
+
195
+ export const workflowEventSchema = z
196
+ .object({
197
+ attempt: z.number().int().min(0),
198
+ detail: z.string().optional(),
199
+ generation: z.number().int().min(1),
200
+ taskHandle: taskHandleSchema,
201
+ timestamp: dateTimeSchema,
202
+ type: z.enum(workflowEventTypeValues),
203
+ })
204
+ .strict()
205
+
206
+ export const finalReportTaskSchema = z
207
+ .object({
208
+ attempt: z.number().int().min(0),
209
+ commitSha: nonEmptyStringSchema.optional(),
210
+ generation: z.number().int().min(1),
211
+ lastReviewVerdict: z.enum(reviewVerdictValues).optional(),
212
+ reason: nonEmptyStringSchema.optional(),
213
+ status: z.enum(taskStatusValues),
214
+ taskHandle: taskHandleSchema,
215
+ })
216
+ .strict()
217
+
218
+ export const finalReportSchema = z
219
+ .object({
220
+ featureId: nonEmptyStringSchema,
221
+ generatedAt: dateTimeSchema,
222
+ tasks: z.array(finalReportTaskSchema),
223
+ summary: z
224
+ .object({
225
+ blockedTasks: z.number().int().min(0),
226
+ completedTasks: z.number().int().min(0),
227
+ finalStatus: z.enum(finalStatusValues),
228
+ replanTasks: z.number().int().min(0),
229
+ totalTasks: z.number().int().min(0),
230
+ })
231
+ .strict(),
232
+ })
233
+ .strict()
@@ -0,0 +1,93 @@
1
+ import { z } from 'zod'
2
+
3
+ export const nonEmptyStringSchema = z.string().min(1)
4
+ export const taskHandleSchema = nonEmptyStringSchema
5
+ export const dateTimeSchema = z.string().datetime({ offset: true })
6
+
7
+ export const taskStatusValues = [
8
+ 'pending',
9
+ 'running',
10
+ 'rework',
11
+ 'done',
12
+ 'blocked',
13
+ 'replan',
14
+ ] as const
15
+ export const runningStageValues = ['implement', 'review', 'integrate'] as const
16
+ export const reviewVerdictValues = [
17
+ 'blocked',
18
+ 'pass',
19
+ 'replan',
20
+ 'rework',
21
+ ] as const
22
+ export const implementStatusValues = [
23
+ 'blocked',
24
+ 'implemented',
25
+ 'partial',
26
+ ] as const
27
+ export const findingSeverityValues = ['high', 'low', 'medium'] as const
28
+ export const acceptanceStatusValues = ['fail', 'pass', 'unclear'] as const
29
+ export const overallRiskValues = ['high', 'low', 'medium'] as const
30
+ export const workflowEventTypeValues = [
31
+ 'attempt_started',
32
+ 'implement_succeeded',
33
+ 'implement_failed',
34
+ 'review_started',
35
+ 'review_completed',
36
+ 'review_failed',
37
+ 'integrate_started',
38
+ 'integrate_completed',
39
+ 'integrate_failed',
40
+ ] as const
41
+ export const finalStatusValues = [
42
+ 'blocked',
43
+ 'completed',
44
+ 'in_progress',
45
+ 'replan_required',
46
+ ] as const
47
+
48
+ function uniqueStrings(items: string[], label: string, ctx: z.RefinementCtx) {
49
+ const seen = new Set<string>()
50
+ for (const [index, item] of items.entries()) {
51
+ if (seen.has(item)) {
52
+ ctx.addIssue({
53
+ code: z.ZodIssueCode.custom,
54
+ message: `Duplicate ${label}: ${item}`,
55
+ path: [index],
56
+ })
57
+ continue
58
+ }
59
+ seen.add(item)
60
+ }
61
+ }
62
+
63
+ export function uniqueStringArray(
64
+ label: string,
65
+ options?: { minItems?: number },
66
+ ) {
67
+ const base = z.array(nonEmptyStringSchema)
68
+ const withMin = options?.minItems ? base.min(options.minItems) : base
69
+ return withMin.superRefine((items, ctx) => {
70
+ uniqueStrings(items, label, ctx)
71
+ })
72
+ }
73
+
74
+ export function formatPath(path: (number | string)[]) {
75
+ if (path.length === 0) {
76
+ return '/'
77
+ }
78
+ return `/${path.join('/')}`
79
+ }
80
+
81
+ export function formatIssues(error: z.ZodError) {
82
+ return error.issues
83
+ .map((issue) => `${formatPath(issue.path)} ${issue.message}`.trim())
84
+ .join('; ')
85
+ }
86
+
87
+ export function parseWithSchema<T>(schema: z.ZodType<T>, value: unknown): T {
88
+ const result = schema.safeParse(value)
89
+ if (!result.success) {
90
+ throw new Error(formatIssues(result.error))
91
+ }
92
+ return result.data
93
+ }
@@ -0,0 +1,79 @@
1
+ import { execa } from 'execa'
2
+
3
+ export interface OpenSpecCliTask {
4
+ description: string
5
+ id: string
6
+ }
7
+
8
+ export interface OpenSpecApplyInstructions {
9
+ changeName: string
10
+ contextFiles: Record<string, string>
11
+ instruction: string
12
+ progress: {
13
+ complete: number
14
+ total: number
15
+ }
16
+ schemaName: string
17
+ state: string
18
+ tasks: OpenSpecCliTask[]
19
+ }
20
+
21
+ export interface OpenSpecStatus {
22
+ applyRequires: string[]
23
+ artifacts: {
24
+ id: string
25
+ outputPath: string
26
+ status: string
27
+ }[]
28
+ changeName: string
29
+ isComplete: boolean
30
+ schemaName: string
31
+ }
32
+
33
+ export async function parseCliJson<T>(stdout: string): Promise<T> {
34
+ const start = stdout.indexOf('{')
35
+ if (start === -1) {
36
+ throw new Error('OpenSpec CLI did not return JSON payload')
37
+ }
38
+ return JSON.parse(stdout.slice(start)) as T
39
+ }
40
+
41
+ export async function readOpenSpecApplyInstructions(input: {
42
+ changeName: string
43
+ workspaceRoot: string
44
+ }): Promise<OpenSpecApplyInstructions> {
45
+ try {
46
+ const { stdout } = await execa(
47
+ 'openspec',
48
+ ['instructions', 'apply', '--change', input.changeName, '--json'],
49
+ {
50
+ cwd: input.workspaceRoot,
51
+ },
52
+ )
53
+ return await parseCliJson<OpenSpecApplyInstructions>(stdout)
54
+ } catch (error) {
55
+ throw new Error(
56
+ `Failed to read OpenSpec apply instructions for ${input.changeName}: ${error instanceof Error ? error.message : String(error)}`,
57
+ )
58
+ }
59
+ }
60
+
61
+ export async function readOpenSpecStatus(input: {
62
+ changeName: string
63
+ workspaceRoot: string
64
+ }): Promise<OpenSpecStatus> {
65
+ try {
66
+ const { stdout } = await execa(
67
+ 'openspec',
68
+ ['status', '--change', input.changeName, '--json'],
69
+ {
70
+ cwd: input.workspaceRoot,
71
+ },
72
+ )
73
+ return await parseCliJson<OpenSpecStatus>(stdout)
74
+ } catch (error) {
75
+ throw new Error(
76
+ `Failed to read OpenSpec status for ${input.changeName}: ${error instanceof Error ? error.message : String(error)}`,
77
+ )
78
+ }
79
+ }
@@ -0,0 +1,121 @@
1
+ import { readdir, readFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ function normalizeGlobPath(value: string) {
5
+ return value.replaceAll(path.sep, '/')
6
+ }
7
+
8
+ function escapeRegex(value: string) {
9
+ return value.replace(/[.+^${}()|[\]\\]/g, String.raw`\$&`)
10
+ }
11
+
12
+ function globToRegExp(pattern: string) {
13
+ const normalized = normalizeGlobPath(pattern)
14
+ let source = ''
15
+ for (let index = 0; index < normalized.length; index += 1) {
16
+ const char = normalized[index] ?? ''
17
+ const next = normalized[index + 1]
18
+ const nextNext = normalized[index + 2]
19
+ if (char === '*' && next === '*' && nextNext === '/') {
20
+ source += '(?:[^/]+/)*'
21
+ index += 2
22
+ continue
23
+ }
24
+ if (char === '*' && next === '*') {
25
+ source += '.*'
26
+ index += 1
27
+ continue
28
+ }
29
+ if (char === '*') {
30
+ source += '[^/]*'
31
+ continue
32
+ }
33
+ source += escapeRegex(char)
34
+ }
35
+ return new RegExp(`^${source}$`)
36
+ }
37
+
38
+ async function listFilesRecursively(root: string): Promise<string[]> {
39
+ const entries = await readdir(root, { withFileTypes: true })
40
+ const files = await Promise.all(
41
+ entries.map(async (entry) => {
42
+ const entryPath = path.join(root, entry.name)
43
+ if (entry.isDirectory()) {
44
+ return listFilesRecursively(entryPath)
45
+ }
46
+ return [entryPath]
47
+ }),
48
+ )
49
+ return files.flat()
50
+ }
51
+
52
+ async function expandPattern(
53
+ baseDir: string,
54
+ pattern: string,
55
+ ): Promise<string[]> {
56
+ const isAbsolutePattern = path.isAbsolute(pattern)
57
+ const matchRoot = isAbsolutePattern ? path.parse(pattern).root : baseDir
58
+
59
+ if (!pattern.includes('*')) {
60
+ return [isAbsolutePattern ? pattern : path.join(baseDir, pattern)]
61
+ }
62
+
63
+ const normalizedPattern = normalizeGlobPath(
64
+ isAbsolutePattern ? path.relative(matchRoot, pattern) : pattern,
65
+ )
66
+ const firstGlobIndex = normalizedPattern.search(/\*/)
67
+ const slashIndex =
68
+ firstGlobIndex === -1
69
+ ? normalizedPattern.length
70
+ : normalizedPattern.lastIndexOf('/', firstGlobIndex)
71
+ const searchPrefix =
72
+ slashIndex <= 0 ? '.' : normalizedPattern.slice(0, slashIndex)
73
+ const searchRoot = path.join(matchRoot, searchPrefix)
74
+ const regex = globToRegExp(normalizedPattern)
75
+
76
+ try {
77
+ const files = await listFilesRecursively(searchRoot)
78
+ return files
79
+ .filter((filePath) =>
80
+ regex.test(normalizeGlobPath(path.relative(matchRoot, filePath))),
81
+ )
82
+ .sort((left, right) =>
83
+ normalizeGlobPath(path.relative(matchRoot, left)).localeCompare(
84
+ normalizeGlobPath(path.relative(matchRoot, right)),
85
+ ),
86
+ )
87
+ } catch {
88
+ return []
89
+ }
90
+ }
91
+
92
+ async function readPattern(baseDir: string, pattern: string) {
93
+ const filePaths = await expandPattern(baseDir, pattern)
94
+ const contents = await Promise.all(
95
+ filePaths.map((filePath) => readFile(filePath, 'utf8')),
96
+ )
97
+ return contents.join('\n\n')
98
+ }
99
+
100
+ export async function readContextFileMap(
101
+ baseDir: string,
102
+ contextFiles: Record<string, string>,
103
+ ): Promise<Map<string, string>> {
104
+ const orderedKeys = ['proposal', 'design', 'specs', 'tasks']
105
+ const remainingKeys = Object.keys(contextFiles).filter(
106
+ (key) => !orderedKeys.includes(key),
107
+ )
108
+ const finalKeys = [
109
+ ...orderedKeys.filter((key) => key in contextFiles),
110
+ ...remainingKeys,
111
+ ]
112
+
113
+ const entries = await Promise.all(
114
+ finalKeys.map(
115
+ async (key) =>
116
+ [key, await readPattern(baseDir, contextFiles[key]!)] as const,
117
+ ),
118
+ )
119
+
120
+ return new Map(entries)
121
+ }
@@ -0,0 +1,57 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ export interface OpenSpecTask {
4
+ checked: boolean
5
+ handle: string
6
+ ordinal: number
7
+ rawLine: string
8
+ sectionTitle: string
9
+ title: string
10
+ }
11
+
12
+ function createTask(
13
+ line: string,
14
+ ordinal: number,
15
+ sectionTitle: string,
16
+ ): OpenSpecTask {
17
+ const checkboxMatch = line.match(/^[-*]\s+\[([ x])\]\s+(\S.*)$/i)
18
+ if (!checkboxMatch) {
19
+ throw new Error(`Invalid task line: ${line}`)
20
+ }
21
+ const body = checkboxMatch[2]!
22
+ const numberedMatch = body.match(/^(\d+(?:\.\d+)*)\s+(\S.*)$/)
23
+
24
+ return {
25
+ checked: checkboxMatch[1]!.toLowerCase() === 'x',
26
+ handle: numberedMatch?.[1] ?? `task-${ordinal}`,
27
+ ordinal,
28
+ rawLine: line,
29
+ sectionTitle,
30
+ title: numberedMatch?.[2] ?? body,
31
+ }
32
+ }
33
+
34
+ export async function parseTasksMd(tasksPath: string): Promise<OpenSpecTask[]> {
35
+ const content = await readFile(tasksPath, 'utf8')
36
+ const lines = content.split(/\r?\n/)
37
+ const tasks: OpenSpecTask[] = []
38
+ let currentSectionTitle = 'unknown'
39
+
40
+ for (const rawLine of lines) {
41
+ const line = rawLine.trimEnd()
42
+ if (line.startsWith('## ')) {
43
+ currentSectionTitle = line.replace(/^##\s+/, '').trim()
44
+ continue
45
+ }
46
+ if (!line.match(/^[-*]\s+\[[ x]\]\s+/i)) {
47
+ continue
48
+ }
49
+ tasks.push(createTask(line, tasks.length + 1, currentSectionTitle))
50
+ }
51
+
52
+ if (tasks.length === 0) {
53
+ throw new Error('No tasks found in tasks.md')
54
+ }
55
+
56
+ return tasks
57
+ }