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,175 @@
|
|
|
1
|
+
import type { TaskSourceSession } from '../task-sources/types'
|
|
2
|
+
import type {
|
|
3
|
+
FinalReport,
|
|
4
|
+
ImplementArtifact,
|
|
5
|
+
IntegrateArtifact,
|
|
6
|
+
ReviewArtifact,
|
|
7
|
+
TaskGraph,
|
|
8
|
+
WorkflowEvent,
|
|
9
|
+
WorkflowState,
|
|
10
|
+
} from '../types'
|
|
11
|
+
|
|
12
|
+
export interface AttemptArtifactKey {
|
|
13
|
+
attempt: number
|
|
14
|
+
generation: number
|
|
15
|
+
taskHandle: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkflowStore {
|
|
19
|
+
appendEvent: (event: WorkflowEvent) => Promise<void>
|
|
20
|
+
loadGraph: () => Promise<null | TaskGraph>
|
|
21
|
+
loadImplementArtifact: (
|
|
22
|
+
key: AttemptArtifactKey,
|
|
23
|
+
) => Promise<ImplementArtifact | null>
|
|
24
|
+
loadReviewArtifact: (
|
|
25
|
+
key: AttemptArtifactKey,
|
|
26
|
+
) => Promise<null | ReviewArtifact>
|
|
27
|
+
loadState: () => Promise<null | WorkflowState>
|
|
28
|
+
readReport: () => Promise<FinalReport | null>
|
|
29
|
+
reset: () => Promise<void>
|
|
30
|
+
saveGraph: (graph: TaskGraph) => Promise<void>
|
|
31
|
+
saveImplementArtifact: (artifact: ImplementArtifact) => Promise<void>
|
|
32
|
+
saveIntegrateArtifact: (artifact: IntegrateArtifact) => Promise<void>
|
|
33
|
+
saveReport: (report: FinalReport) => Promise<void>
|
|
34
|
+
saveReviewArtifact: (artifact: ReviewArtifact) => Promise<void>
|
|
35
|
+
saveState: (state: WorkflowState) => Promise<void>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GitCheckoutBranchOptions {
|
|
39
|
+
create?: boolean
|
|
40
|
+
startPoint?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GitCommitTaskInput {
|
|
44
|
+
message: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface GitCommitTaskResult {
|
|
48
|
+
commitSha: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GitPushBranchOptions {
|
|
52
|
+
setUpstream?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface GitPort {
|
|
56
|
+
checkoutBranch: (
|
|
57
|
+
name: string,
|
|
58
|
+
options?: GitCheckoutBranchOptions,
|
|
59
|
+
) => Promise<void>
|
|
60
|
+
checkoutRemoteBranch: (name: string) => Promise<void>
|
|
61
|
+
commitTask: (input: GitCommitTaskInput) => Promise<GitCommitTaskResult>
|
|
62
|
+
deleteLocalBranch: (name: string) => Promise<void>
|
|
63
|
+
getChangedFilesSinceHead: () => Promise<string[]>
|
|
64
|
+
getCurrentBranch: () => Promise<string>
|
|
65
|
+
getHeadSha: () => Promise<string>
|
|
66
|
+
getHeadSubject: () => Promise<string>
|
|
67
|
+
getHeadTimestamp: () => Promise<string>
|
|
68
|
+
pullFastForward: (branch: string) => Promise<void>
|
|
69
|
+
pushBranch: (name: string, options?: GitPushBranchOptions) => Promise<void>
|
|
70
|
+
requireCleanWorktree: () => Promise<void>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PullRequestRef {
|
|
74
|
+
number: number
|
|
75
|
+
title: string
|
|
76
|
+
url: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface MergedPullRequestRef extends PullRequestRef {
|
|
80
|
+
mergeCommitSha: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PullRequestReaction {
|
|
84
|
+
content: string
|
|
85
|
+
createdAt: string
|
|
86
|
+
userLogin: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PullRequestReviewSummary {
|
|
90
|
+
body: string
|
|
91
|
+
id: number
|
|
92
|
+
state: string
|
|
93
|
+
submittedAt: null | string
|
|
94
|
+
url: string
|
|
95
|
+
userLogin: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface PullRequestDiscussionComment {
|
|
99
|
+
body: string
|
|
100
|
+
createdAt: string
|
|
101
|
+
id: number
|
|
102
|
+
url: string
|
|
103
|
+
userLogin: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface PullRequestReviewThreadComment {
|
|
107
|
+
body: string
|
|
108
|
+
createdAt: string
|
|
109
|
+
line: null | number
|
|
110
|
+
path: string
|
|
111
|
+
url: string
|
|
112
|
+
userLogin: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface PullRequestReviewThread {
|
|
116
|
+
comments: PullRequestReviewThreadComment[]
|
|
117
|
+
id: string
|
|
118
|
+
isOutdated: boolean
|
|
119
|
+
isResolved: boolean
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface PullRequestSnapshot {
|
|
123
|
+
changedFiles: string[]
|
|
124
|
+
discussionComments: PullRequestDiscussionComment[]
|
|
125
|
+
reactions: PullRequestReaction[]
|
|
126
|
+
reviewSummaries: PullRequestReviewSummary[]
|
|
127
|
+
reviewThreads: PullRequestReviewThread[]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface GitHubPort {
|
|
131
|
+
createPullRequest: (input: CreatePullRequestInput) => Promise<PullRequestRef>
|
|
132
|
+
findMergedPullRequestByHeadBranch: (
|
|
133
|
+
input: FindMergedPullRequestByHeadBranchInput,
|
|
134
|
+
) => Promise<MergedPullRequestRef | null>
|
|
135
|
+
findOpenPullRequestByHeadBranch: (
|
|
136
|
+
input: FindOpenPullRequestByHeadBranchInput,
|
|
137
|
+
) => Promise<null | PullRequestRef>
|
|
138
|
+
getPullRequestSnapshot: (
|
|
139
|
+
input: GetPullRequestSnapshotInput,
|
|
140
|
+
) => Promise<PullRequestSnapshot>
|
|
141
|
+
squashMergePullRequest: (
|
|
142
|
+
input: SquashMergePullRequestInput,
|
|
143
|
+
) => Promise<GitCommitTaskResult>
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface CreatePullRequestInput {
|
|
147
|
+
baseBranch: string
|
|
148
|
+
body: string
|
|
149
|
+
headBranch: string
|
|
150
|
+
title: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface FindMergedPullRequestByHeadBranchInput {
|
|
154
|
+
headBranch: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface FindOpenPullRequestByHeadBranchInput {
|
|
158
|
+
headBranch: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface GetPullRequestSnapshotInput {
|
|
162
|
+
pullRequestNumber: number
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface SquashMergePullRequestInput {
|
|
166
|
+
pullRequestNumber: number
|
|
167
|
+
subject: string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface OrchestratorRuntime {
|
|
171
|
+
git: GitPort
|
|
172
|
+
github: GitHubPort
|
|
173
|
+
store: WorkflowStore
|
|
174
|
+
taskSource: TaskSourceSession
|
|
175
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { TaskSourceSession } from '../task-sources/types'
|
|
2
|
+
|
|
3
|
+
export interface TaskTopologyEntry {
|
|
4
|
+
commitSubject: string
|
|
5
|
+
dependsOn: string[]
|
|
6
|
+
handle: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TaskTopology {
|
|
10
|
+
featureId: string
|
|
11
|
+
maxIterations: number
|
|
12
|
+
tasks: TaskTopologyEntry[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureUniqueHandles(handles: string[]) {
|
|
16
|
+
const seen = new Set<string>()
|
|
17
|
+
for (const handle of handles) {
|
|
18
|
+
if (seen.has(handle)) {
|
|
19
|
+
throw new Error(`Duplicate task handle: ${handle}`)
|
|
20
|
+
}
|
|
21
|
+
seen.add(handle)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensureDependenciesExist(tasks: TaskTopologyEntry[]) {
|
|
26
|
+
const handles = new Set(tasks.map((task) => task.handle))
|
|
27
|
+
for (const task of tasks) {
|
|
28
|
+
for (const dependency of task.dependsOn) {
|
|
29
|
+
if (!handles.has(dependency)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Unknown dependency: ${dependency} for task ${task.handle}`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ensureAcyclic(tasks: TaskTopologyEntry[]) {
|
|
39
|
+
const graph = new Map(tasks.map((task) => [task.handle, task.dependsOn]))
|
|
40
|
+
const visiting = new Set<string>()
|
|
41
|
+
const visited = new Set<string>()
|
|
42
|
+
|
|
43
|
+
const visit = (handle: string) => {
|
|
44
|
+
if (visiting.has(handle)) {
|
|
45
|
+
throw new Error(`Dependency cycle detected at ${handle}`)
|
|
46
|
+
}
|
|
47
|
+
if (visited.has(handle)) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
visiting.add(handle)
|
|
51
|
+
for (const dependency of graph.get(handle) ?? []) {
|
|
52
|
+
visit(dependency)
|
|
53
|
+
}
|
|
54
|
+
visiting.delete(handle)
|
|
55
|
+
visited.add(handle)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const task of tasks) {
|
|
59
|
+
visit(task.handle)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildTaskTopology(
|
|
64
|
+
session: TaskSourceSession,
|
|
65
|
+
featureId: string,
|
|
66
|
+
maxIterations: number,
|
|
67
|
+
): TaskTopology {
|
|
68
|
+
const handles = session.listTasks()
|
|
69
|
+
ensureUniqueHandles(handles)
|
|
70
|
+
|
|
71
|
+
const tasks = handles.map((handle) => ({
|
|
72
|
+
commitSubject: session.buildCommitSubject(handle),
|
|
73
|
+
dependsOn: session.getTaskDependencies(handle),
|
|
74
|
+
handle,
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
ensureDependenciesExist(tasks)
|
|
78
|
+
ensureAcyclic(tasks)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
featureId,
|
|
82
|
+
maxIterations,
|
|
83
|
+
tasks,
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { inspect } from 'node:util'
|
|
2
|
+
|
|
3
|
+
import arg from 'arg'
|
|
4
|
+
|
|
5
|
+
import { runBatchCommand } from './commands/batch'
|
|
6
|
+
import { runCommand } from './commands/run'
|
|
7
|
+
import { resolveWorkspaceContext } from './runtime/workspace-resolver'
|
|
8
|
+
import { loadWorkflowConfig } from './workflow/config'
|
|
9
|
+
|
|
10
|
+
interface PositionalArgs {
|
|
11
|
+
_: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RunOptions {
|
|
15
|
+
feature?: string
|
|
16
|
+
untilTaskId?: string
|
|
17
|
+
verbose: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BatchOptions {
|
|
21
|
+
configPath: string
|
|
22
|
+
verbose: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertNoPositionalArgs(values: PositionalArgs) {
|
|
26
|
+
if (values._.length !== 0) {
|
|
27
|
+
throw new Error(`Unexpected positional arguments: ${values._.join(' ')}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseRunOptions(args: string[]) {
|
|
32
|
+
const values = arg(
|
|
33
|
+
{
|
|
34
|
+
'--feature': String,
|
|
35
|
+
'--until-task': String,
|
|
36
|
+
'--verbose': Boolean,
|
|
37
|
+
},
|
|
38
|
+
{ argv: args },
|
|
39
|
+
)
|
|
40
|
+
assertNoPositionalArgs(values)
|
|
41
|
+
const options: RunOptions = {
|
|
42
|
+
verbose: values['--verbose'] ?? false,
|
|
43
|
+
}
|
|
44
|
+
if (values['--until-task']) {
|
|
45
|
+
options.untilTaskId = values['--until-task']
|
|
46
|
+
}
|
|
47
|
+
if (values['--feature']) {
|
|
48
|
+
options.feature = values['--feature']
|
|
49
|
+
}
|
|
50
|
+
return options
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseBatchOptions(args: string[]) {
|
|
54
|
+
const values = arg(
|
|
55
|
+
{
|
|
56
|
+
'--config': String,
|
|
57
|
+
'--verbose': Boolean,
|
|
58
|
+
},
|
|
59
|
+
{ argv: args },
|
|
60
|
+
)
|
|
61
|
+
assertNoPositionalArgs(values)
|
|
62
|
+
const configPath = values['--config']
|
|
63
|
+
if (!configPath) {
|
|
64
|
+
throw new Error('Missing required --config')
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
configPath,
|
|
68
|
+
verbose: values['--verbose'] ?? false,
|
|
69
|
+
} satisfies BatchOptions
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
73
|
+
const [command = 'run', ...args] = argv
|
|
74
|
+
switch (command) {
|
|
75
|
+
case 'batch': {
|
|
76
|
+
const options = parseBatchOptions(args)
|
|
77
|
+
const result = await runBatchCommand({
|
|
78
|
+
configPath: options.configPath,
|
|
79
|
+
cwd: process.cwd(),
|
|
80
|
+
verbose: options.verbose,
|
|
81
|
+
})
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
`${inspect(result, { colors: false, depth: null })}\n`,
|
|
84
|
+
)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
case 'run': {
|
|
88
|
+
const options = parseRunOptions(args)
|
|
89
|
+
const config = await loadWorkflowConfig(process.cwd())
|
|
90
|
+
const context = await resolveWorkspaceContext({
|
|
91
|
+
cwd: process.cwd(),
|
|
92
|
+
...(options.feature ? { feature: options.feature } : {}),
|
|
93
|
+
taskSource: config.task.source,
|
|
94
|
+
})
|
|
95
|
+
const result = await runCommand(context, {
|
|
96
|
+
config,
|
|
97
|
+
...(options.untilTaskId ? { untilTaskId: options.untilTaskId } : {}),
|
|
98
|
+
verbose: options.verbose,
|
|
99
|
+
})
|
|
100
|
+
process.stdout.write(
|
|
101
|
+
`${inspect(result, { colors: false, depth: null })}\n`,
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Unknown command: ${command}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function main() {
|
|
111
|
+
return runCli()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function handleFatalError(error: unknown) {
|
|
115
|
+
process.stderr.write(
|
|
116
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
117
|
+
)
|
|
118
|
+
process.exitCode = 1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
void main().catch(handleFatalError)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ImplementAgentInput } from '../agents/types'
|
|
2
|
+
|
|
3
|
+
export async function buildImplementerPrompt(input: ImplementAgentInput) {
|
|
4
|
+
return [
|
|
5
|
+
'Return JSON only.',
|
|
6
|
+
...input.prompt.instructions,
|
|
7
|
+
`Task Handle: ${input.taskHandle}`,
|
|
8
|
+
...[
|
|
9
|
+
...input.prompt.sections,
|
|
10
|
+
{ content: String(input.attempt), title: 'Attempt' },
|
|
11
|
+
{ content: String(input.generation), title: 'Generation' },
|
|
12
|
+
{
|
|
13
|
+
content: JSON.stringify(input.lastFindings),
|
|
14
|
+
title: 'Previous Findings',
|
|
15
|
+
},
|
|
16
|
+
].map((section) => `${section.title}:\n${section.content}`),
|
|
17
|
+
].join('\n\n')
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReviewAgentInput } from '../agents/types'
|
|
2
|
+
|
|
3
|
+
export async function buildReviewerPrompt(input: ReviewAgentInput) {
|
|
4
|
+
return [
|
|
5
|
+
'Return JSON only.',
|
|
6
|
+
...input.prompt.instructions,
|
|
7
|
+
`Task Handle: ${input.taskHandle}`,
|
|
8
|
+
...[
|
|
9
|
+
...input.prompt.sections,
|
|
10
|
+
{ content: String(input.attempt), title: 'Attempt' },
|
|
11
|
+
{ content: String(input.generation), title: 'Generation' },
|
|
12
|
+
{
|
|
13
|
+
content: JSON.stringify(input.lastFindings),
|
|
14
|
+
title: 'Previous Findings',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
content: JSON.stringify(input.actualChangedFiles),
|
|
18
|
+
title: 'Actual Changed Files',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
content: JSON.stringify(input.implement),
|
|
22
|
+
title: 'Implement Result',
|
|
23
|
+
},
|
|
24
|
+
].map((section) => `${section.title}:\n${section.content}`),
|
|
25
|
+
].join('\n\n')
|
|
26
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { appendFile, readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import * as fsExtra from 'fs-extra'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
validateFinalReport,
|
|
8
|
+
validateImplementArtifact,
|
|
9
|
+
validateIntegrateArtifact,
|
|
10
|
+
validateReviewArtifact,
|
|
11
|
+
validateTaskGraph,
|
|
12
|
+
validateWorkflowEvent,
|
|
13
|
+
validateWorkflowState,
|
|
14
|
+
} from '../schema/index'
|
|
15
|
+
import { writeJsonAtomic } from '../utils/fs'
|
|
16
|
+
import { GitRuntime } from './git'
|
|
17
|
+
import { GitHubRuntime } from './github'
|
|
18
|
+
import { createRuntimePaths } from './path-layout'
|
|
19
|
+
|
|
20
|
+
import type { AttemptArtifactKey, OrchestratorRuntime } from '../core/runtime'
|
|
21
|
+
import type { TaskSourceSession } from '../task-sources/types'
|
|
22
|
+
|
|
23
|
+
function createArtifactDir(
|
|
24
|
+
featureDir: string,
|
|
25
|
+
taskId: string,
|
|
26
|
+
generation: number,
|
|
27
|
+
attempt: number,
|
|
28
|
+
) {
|
|
29
|
+
const runtimePaths = createRuntimePaths(featureDir)
|
|
30
|
+
return path.join(
|
|
31
|
+
runtimePaths.tasksDir,
|
|
32
|
+
taskId,
|
|
33
|
+
`g${generation}`,
|
|
34
|
+
`a${attempt}`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readTextFileIfExists(filePath: string) {
|
|
39
|
+
const exists = await fsExtra.pathExists(filePath)
|
|
40
|
+
if (!exists) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
return readFile(filePath, 'utf8')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readValidatedJsonFileIfExists<T>(
|
|
47
|
+
filePath: string,
|
|
48
|
+
validate: (value: unknown) => T,
|
|
49
|
+
): Promise<null | T> {
|
|
50
|
+
const raw = await readTextFileIfExists(filePath)
|
|
51
|
+
if (raw === null) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return validate(JSON.parse(raw))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CreateOrchestratorRuntimeInput {
|
|
58
|
+
featureDir: string
|
|
59
|
+
taskSource?: TaskSourceSession
|
|
60
|
+
workspaceRoot: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createOrchestratorRuntime(
|
|
64
|
+
input: CreateOrchestratorRuntimeInput,
|
|
65
|
+
): OrchestratorRuntime {
|
|
66
|
+
const runtimePaths = createRuntimePaths(input.featureDir)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
git: new GitRuntime(input.workspaceRoot, runtimePaths.runtimeDir),
|
|
70
|
+
github: new GitHubRuntime(input.workspaceRoot),
|
|
71
|
+
store: {
|
|
72
|
+
async appendEvent(event) {
|
|
73
|
+
const value = validateWorkflowEvent(event)
|
|
74
|
+
await fsExtra.ensureDir(path.dirname(runtimePaths.events))
|
|
75
|
+
await appendFile(runtimePaths.events, `${JSON.stringify(value)}\n`)
|
|
76
|
+
},
|
|
77
|
+
async loadGraph() {
|
|
78
|
+
return readValidatedJsonFileIfExists(
|
|
79
|
+
runtimePaths.graph,
|
|
80
|
+
validateTaskGraph,
|
|
81
|
+
)
|
|
82
|
+
},
|
|
83
|
+
async loadImplementArtifact(key: AttemptArtifactKey) {
|
|
84
|
+
const filePath = path.join(
|
|
85
|
+
createArtifactDir(
|
|
86
|
+
input.featureDir,
|
|
87
|
+
key.taskHandle,
|
|
88
|
+
key.generation,
|
|
89
|
+
key.attempt,
|
|
90
|
+
),
|
|
91
|
+
'implement.json',
|
|
92
|
+
)
|
|
93
|
+
return readValidatedJsonFileIfExists(
|
|
94
|
+
filePath,
|
|
95
|
+
validateImplementArtifact,
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
async loadReviewArtifact(key: AttemptArtifactKey) {
|
|
99
|
+
const filePath = path.join(
|
|
100
|
+
createArtifactDir(
|
|
101
|
+
input.featureDir,
|
|
102
|
+
key.taskHandle,
|
|
103
|
+
key.generation,
|
|
104
|
+
key.attempt,
|
|
105
|
+
),
|
|
106
|
+
'review.json',
|
|
107
|
+
)
|
|
108
|
+
return readValidatedJsonFileIfExists(filePath, validateReviewArtifact)
|
|
109
|
+
},
|
|
110
|
+
async loadState() {
|
|
111
|
+
return readValidatedJsonFileIfExists(
|
|
112
|
+
runtimePaths.state,
|
|
113
|
+
validateWorkflowState,
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
async readReport() {
|
|
117
|
+
return readValidatedJsonFileIfExists(
|
|
118
|
+
runtimePaths.report,
|
|
119
|
+
validateFinalReport,
|
|
120
|
+
)
|
|
121
|
+
},
|
|
122
|
+
async reset() {
|
|
123
|
+
await fsExtra.remove(runtimePaths.runtimeDir)
|
|
124
|
+
},
|
|
125
|
+
async saveGraph(graph) {
|
|
126
|
+
await writeJsonAtomic(runtimePaths.graph, validateTaskGraph(graph))
|
|
127
|
+
},
|
|
128
|
+
async saveImplementArtifact(artifact) {
|
|
129
|
+
const value = validateImplementArtifact(artifact)
|
|
130
|
+
const targetPath = path.join(
|
|
131
|
+
createArtifactDir(
|
|
132
|
+
input.featureDir,
|
|
133
|
+
artifact.taskHandle,
|
|
134
|
+
artifact.generation,
|
|
135
|
+
artifact.attempt,
|
|
136
|
+
),
|
|
137
|
+
'implement.json',
|
|
138
|
+
)
|
|
139
|
+
await writeJsonAtomic(targetPath, value)
|
|
140
|
+
},
|
|
141
|
+
async saveIntegrateArtifact(artifact) {
|
|
142
|
+
const value = validateIntegrateArtifact(artifact)
|
|
143
|
+
const targetPath = path.join(
|
|
144
|
+
createArtifactDir(
|
|
145
|
+
input.featureDir,
|
|
146
|
+
artifact.taskHandle,
|
|
147
|
+
artifact.generation,
|
|
148
|
+
artifact.attempt,
|
|
149
|
+
),
|
|
150
|
+
'integrate.json',
|
|
151
|
+
)
|
|
152
|
+
await writeJsonAtomic(targetPath, value)
|
|
153
|
+
},
|
|
154
|
+
async saveReport(report) {
|
|
155
|
+
await writeJsonAtomic(runtimePaths.report, validateFinalReport(report))
|
|
156
|
+
},
|
|
157
|
+
async saveReviewArtifact(artifact) {
|
|
158
|
+
const value = validateReviewArtifact(artifact)
|
|
159
|
+
const targetPath = path.join(
|
|
160
|
+
createArtifactDir(
|
|
161
|
+
input.featureDir,
|
|
162
|
+
artifact.taskHandle,
|
|
163
|
+
artifact.generation,
|
|
164
|
+
artifact.attempt,
|
|
165
|
+
),
|
|
166
|
+
'review.json',
|
|
167
|
+
)
|
|
168
|
+
await writeJsonAtomic(targetPath, value)
|
|
169
|
+
},
|
|
170
|
+
async saveState(state) {
|
|
171
|
+
await writeJsonAtomic(runtimePaths.state, validateWorkflowState(state))
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
taskSource:
|
|
175
|
+
input.taskSource ??
|
|
176
|
+
({
|
|
177
|
+
async applyTaskCompletion() {
|
|
178
|
+
throw new Error('task source is not configured')
|
|
179
|
+
},
|
|
180
|
+
buildCommitSubject() {
|
|
181
|
+
throw new Error('task source is not configured')
|
|
182
|
+
},
|
|
183
|
+
async buildImplementPrompt() {
|
|
184
|
+
throw new Error('task source is not configured')
|
|
185
|
+
},
|
|
186
|
+
async buildReviewPrompt() {
|
|
187
|
+
throw new Error('task source is not configured')
|
|
188
|
+
},
|
|
189
|
+
async getCompletionCriteria() {
|
|
190
|
+
throw new Error('task source is not configured')
|
|
191
|
+
},
|
|
192
|
+
getTaskDependencies() {
|
|
193
|
+
throw new Error('task source is not configured')
|
|
194
|
+
},
|
|
195
|
+
async isTaskCompleted() {
|
|
196
|
+
throw new Error('task source is not configured')
|
|
197
|
+
},
|
|
198
|
+
listTasks() {
|
|
199
|
+
throw new Error('task source is not configured')
|
|
200
|
+
},
|
|
201
|
+
resolveTaskSelector() {
|
|
202
|
+
throw new Error('task source is not configured')
|
|
203
|
+
},
|
|
204
|
+
async revertTaskCompletion() {
|
|
205
|
+
throw new Error('task source is not configured')
|
|
206
|
+
},
|
|
207
|
+
} satisfies TaskSourceSession),
|
|
208
|
+
}
|
|
209
|
+
}
|