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,137 @@
1
+ import path from 'node:path'
2
+
3
+ import { execa } from 'execa'
4
+
5
+ import { filterPorcelainStatus } from '../utils/fs'
6
+
7
+ import type {
8
+ GitCheckoutBranchOptions,
9
+ GitCommitTaskInput,
10
+ GitCommitTaskResult,
11
+ GitPort,
12
+ GitPushBranchOptions,
13
+ } from '../core/runtime'
14
+
15
+ function normalizeRelativePath(value: string) {
16
+ return value.split(path.sep).join('/')
17
+ }
18
+
19
+ async function runGit(cwd: string, args: string[]) {
20
+ const result = await execa('git', args, { cwd })
21
+ return result.stdout.trim()
22
+ }
23
+
24
+ export class GitRuntime implements GitPort {
25
+ private readonly runtimeDirRelative: string
26
+
27
+ public constructor(
28
+ private readonly workspaceRoot: string,
29
+ runtimeDir: string,
30
+ ) {
31
+ this.runtimeDirRelative = normalizeRelativePath(
32
+ path.relative(this.workspaceRoot, runtimeDir),
33
+ )
34
+ }
35
+
36
+ public async checkoutBranch(
37
+ name: string,
38
+ options?: GitCheckoutBranchOptions,
39
+ ) {
40
+ if (options?.create) {
41
+ const args = ['checkout', '-b', name]
42
+ if (options.startPoint) {
43
+ args.push(options.startPoint)
44
+ }
45
+ await runGit(this.workspaceRoot, args)
46
+ return
47
+ }
48
+ await runGit(this.workspaceRoot, ['checkout', name])
49
+ }
50
+
51
+ public async checkoutRemoteBranch(name: string) {
52
+ await runGit(this.workspaceRoot, ['fetch', 'origin', name])
53
+ await runGit(this.workspaceRoot, ['checkout', '-B', name, 'FETCH_HEAD'])
54
+ }
55
+
56
+ public async commitTask(
57
+ input: GitCommitTaskInput,
58
+ ): Promise<GitCommitTaskResult> {
59
+ await runGit(this.workspaceRoot, ['add', '-A', '.'])
60
+ await runGit(this.workspaceRoot, ['reset', '--', this.runtimeDirRelative])
61
+ await runGit(this.workspaceRoot, [
62
+ 'commit',
63
+ '--allow-empty',
64
+ '-m',
65
+ input.message,
66
+ ])
67
+ const commitSha = await runGit(this.workspaceRoot, ['rev-parse', 'HEAD'])
68
+ return { commitSha }
69
+ }
70
+
71
+ public async deleteLocalBranch(name: string) {
72
+ await runGit(this.workspaceRoot, ['branch', '-D', name])
73
+ }
74
+
75
+ public async getChangedFilesSinceHead() {
76
+ const [changed, untracked] = await Promise.all([
77
+ runGit(this.workspaceRoot, ['diff', '--name-only', 'HEAD']),
78
+ runGit(this.workspaceRoot, [
79
+ 'ls-files',
80
+ '--others',
81
+ '--exclude-standard',
82
+ ]),
83
+ ])
84
+ const files = new Set(
85
+ [...changed.split('\n'), ...untracked.split('\n')]
86
+ .map((item) => item.trim())
87
+ .filter(Boolean)
88
+ .filter(
89
+ (item) =>
90
+ item !== this.runtimeDirRelative &&
91
+ !item.startsWith(`${this.runtimeDirRelative}/`),
92
+ ),
93
+ )
94
+ return [...files].sort()
95
+ }
96
+
97
+ public async getCurrentBranch() {
98
+ return runGit(this.workspaceRoot, ['branch', '--show-current'])
99
+ }
100
+
101
+ public async getHeadSha() {
102
+ return runGit(this.workspaceRoot, ['rev-parse', 'HEAD'])
103
+ }
104
+
105
+ public async getHeadSubject() {
106
+ return runGit(this.workspaceRoot, ['log', '-1', '--format=%s', 'HEAD'])
107
+ }
108
+
109
+ public async getHeadTimestamp() {
110
+ return runGit(this.workspaceRoot, ['log', '-1', '--format=%cI', 'HEAD'])
111
+ }
112
+
113
+ public async pullFastForward(branch: string) {
114
+ await runGit(this.workspaceRoot, ['pull', '--ff-only', 'origin', branch])
115
+ }
116
+
117
+ public async pushBranch(name: string, options?: GitPushBranchOptions) {
118
+ const args = ['push']
119
+ if (options?.setUpstream) {
120
+ args.push('-u')
121
+ }
122
+ args.push('origin', name)
123
+ await runGit(this.workspaceRoot, args)
124
+ }
125
+
126
+ public async requireCleanWorktree() {
127
+ const status = await runGit(this.workspaceRoot, ['status', '--porcelain'])
128
+ const relevantLines = filterPorcelainStatus(
129
+ status.split('\n').filter(Boolean),
130
+ this.runtimeDirRelative,
131
+ )
132
+
133
+ if (relevantLines.length !== 0) {
134
+ throw new Error('Worktree must be clean before running task-while')
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,307 @@
1
+ import type {
2
+ PullRequestDiscussionComment,
3
+ PullRequestReaction,
4
+ PullRequestReviewSummary,
5
+ PullRequestReviewThreadComment,
6
+ } from '../core/runtime'
7
+
8
+ export interface ConnectionPage<T> {
9
+ nodes: T[]
10
+ pageInfo: ConnectionPageInfo
11
+ }
12
+
13
+ export interface ConnectionPageInfo {
14
+ endCursor: null | string
15
+ hasNextPage: boolean
16
+ }
17
+
18
+ export interface PullRequestSnapshotPage {
19
+ comments: ConnectionPage<PullRequestDiscussionComment> | null
20
+ files: ConnectionPage<string> | null
21
+ reactions: ConnectionPage<PullRequestReaction> | null
22
+ reviews: ConnectionPage<PullRequestReviewSummary> | null
23
+ reviewThreads: ConnectionPage<ReviewThreadPageNode> | null
24
+ }
25
+
26
+ export interface ReviewThreadPageNode {
27
+ comments: PullRequestReviewThreadComment[]
28
+ commentsPageInfo: ConnectionPageInfo
29
+ id: string
30
+ isOutdated: boolean
31
+ isResolved: boolean
32
+ }
33
+
34
+ export interface RequestedSnapshotConnections {
35
+ comments: boolean
36
+ files: boolean
37
+ reactions: boolean
38
+ reviews: boolean
39
+ reviewThreads: boolean
40
+ }
41
+
42
+ export interface ParseRequestedConnectionInput<T> {
43
+ label: string
44
+ mapNode: (node: Record<string, unknown>) => T
45
+ requested: boolean
46
+ value: unknown
47
+ }
48
+
49
+ function asBoolean(value: unknown) {
50
+ return value === true
51
+ }
52
+
53
+ function asNullableNumber(value: unknown) {
54
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
55
+ }
56
+
57
+ function asNullableString(value: unknown) {
58
+ return typeof value === 'string' ? value : null
59
+ }
60
+
61
+ function asNumericId(value: unknown) {
62
+ if (typeof value === 'number' && Number.isFinite(value)) {
63
+ return value
64
+ }
65
+ if (typeof value === 'string' && value.trim() !== '') {
66
+ const parsed = Number(value)
67
+ return Number.isFinite(parsed) ? parsed : 0
68
+ }
69
+ return 0
70
+ }
71
+
72
+ function asRecord(value: unknown) {
73
+ return value && typeof value === 'object'
74
+ ? (value as Record<string, unknown>)
75
+ : null
76
+ }
77
+
78
+ function asString(value: unknown) {
79
+ return typeof value === 'string' ? value : ''
80
+ }
81
+
82
+ interface GraphQLReviewThreadCommentsNode {
83
+ comments?: Record<string, unknown>
84
+ }
85
+
86
+ interface GraphQLReviewThreadCommentsData {
87
+ node?: GraphQLReviewThreadCommentsNode
88
+ }
89
+
90
+ interface GraphQLReviewThreadCommentsPayload {
91
+ data?: GraphQLReviewThreadCommentsData
92
+ }
93
+
94
+ interface GraphQLRepositoryPullRequestPayload {
95
+ pullRequest?: Record<string, unknown>
96
+ }
97
+
98
+ interface GraphQLSnapshotRepository {
99
+ repository?: GraphQLRepositoryPullRequestPayload
100
+ }
101
+
102
+ interface GraphQLSnapshotPayload {
103
+ data?: GraphQLSnapshotRepository
104
+ }
105
+
106
+ interface LoginLike {
107
+ login?: unknown
108
+ }
109
+
110
+ function invalidGraphQLError(label: string) {
111
+ return new Error(`Invalid GraphQL connection: ${label}`)
112
+ }
113
+
114
+ function missingGraphQLError(label: string) {
115
+ return new Error(`Missing GraphQL connection: ${label}`)
116
+ }
117
+
118
+ function requirePageInfo(value: unknown, label: string) {
119
+ const record = asRecord(value)
120
+ if (!record) {
121
+ throw invalidGraphQLError(`${label}.pageInfo`)
122
+ }
123
+ if (record.endCursor !== null && typeof record.endCursor !== 'string') {
124
+ throw invalidGraphQLError(`${label}.pageInfo.endCursor`)
125
+ }
126
+ if (typeof record.hasNextPage !== 'boolean') {
127
+ throw invalidGraphQLError(`${label}.pageInfo.hasNextPage`)
128
+ }
129
+ if (record.hasNextPage && typeof record.endCursor !== 'string') {
130
+ throw invalidGraphQLError(`${label}.pageInfo.endCursor`)
131
+ }
132
+ return {
133
+ endCursor: record.endCursor,
134
+ hasNextPage: record.hasNextPage,
135
+ }
136
+ }
137
+
138
+ function requireConnectionRecord(value: unknown, label: string) {
139
+ const record = asRecord(value)
140
+ if (!record) {
141
+ throw missingGraphQLError(label)
142
+ }
143
+ if (!Array.isArray(record.nodes)) {
144
+ throw invalidGraphQLError(`${label}.nodes`)
145
+ }
146
+ return {
147
+ nodes: record.nodes as Record<string, unknown>[],
148
+ pageInfo: requirePageInfo(record.pageInfo, label),
149
+ }
150
+ }
151
+
152
+ function parseConnection<T>(
153
+ value: unknown,
154
+ label: string,
155
+ mapNode: (node: Record<string, unknown>) => T,
156
+ ): ConnectionPage<T> {
157
+ const record = requireConnectionRecord(value, label)
158
+ return {
159
+ nodes: record.nodes.map((node) => mapNode(node)),
160
+ pageInfo: record.pageInfo,
161
+ }
162
+ }
163
+
164
+ function parseRequestedConnection<T>(
165
+ input: ParseRequestedConnectionInput<T>,
166
+ ): ConnectionPage<T> | null {
167
+ if (!input.requested) {
168
+ return null
169
+ }
170
+ return parseConnection(input.value, input.label, input.mapNode)
171
+ }
172
+
173
+ export function parseReviewThreadCommentsPage(
174
+ raw: string,
175
+ ): ConnectionPage<PullRequestReviewThreadComment> {
176
+ const payload = JSON.parse(raw) as GraphQLReviewThreadCommentsPayload
177
+ return parseConnection(
178
+ payload.data?.node?.comments,
179
+ 'reviewThread.comments',
180
+ (node) => toReviewThreadComment(node),
181
+ )
182
+ }
183
+
184
+ export function parseSnapshotPage(
185
+ raw: string,
186
+ requested: RequestedSnapshotConnections,
187
+ ): PullRequestSnapshotPage {
188
+ const payload = JSON.parse(raw) as GraphQLSnapshotPayload
189
+ const repository = asRecord(payload.data?.repository)
190
+ const record = asRecord(repository?.pullRequest)
191
+ if (!record) {
192
+ throw new Error('Missing GraphQL node: repository.pullRequest')
193
+ }
194
+ return {
195
+ comments: parseRequestedConnection({
196
+ label: 'comments',
197
+ requested: requested.comments,
198
+ value: record.comments,
199
+ mapNode: (node) => toDiscussionComment(node),
200
+ }),
201
+ files: parseRequestedConnection({
202
+ label: 'files',
203
+ requested: requested.files,
204
+ value: record.files,
205
+ mapNode: (node) => asString(node.path),
206
+ }),
207
+ reactions: parseRequestedConnection({
208
+ label: 'reactions',
209
+ requested: requested.reactions,
210
+ value: record.reactions,
211
+ mapNode: (node) => toReaction(node),
212
+ }),
213
+ reviews: parseRequestedConnection({
214
+ label: 'reviews',
215
+ requested: requested.reviews,
216
+ value: record.reviews,
217
+ mapNode: (node) => toReviewSummary(node),
218
+ }),
219
+ reviewThreads: parseRequestedConnection({
220
+ label: 'reviewThreads',
221
+ requested: requested.reviewThreads,
222
+ value: record.reviewThreads,
223
+ mapNode: (node) => toReviewThreadPageNode(node),
224
+ }),
225
+ }
226
+ }
227
+
228
+ function toDiscussionComment(
229
+ item: Record<string, unknown>,
230
+ ): PullRequestDiscussionComment {
231
+ return {
232
+ id: asNumericId(item.id ?? item.databaseId),
233
+ body: asString(item.body),
234
+ createdAt: asString(item.created_at ?? item.createdAt),
235
+ url: asString(item.html_url ?? item.url),
236
+ userLogin: asString(
237
+ (item.user as LoginLike | null)?.login ??
238
+ (item.author as LoginLike | null)?.login,
239
+ ),
240
+ }
241
+ }
242
+
243
+ function toReaction(item: Record<string, unknown>): PullRequestReaction {
244
+ return {
245
+ content: toReactionContent(asString(item.content)),
246
+ createdAt: asString(item.created_at ?? item.createdAt),
247
+ userLogin: asString((item.user as LoginLike | null)?.login),
248
+ }
249
+ }
250
+
251
+ function toReactionContent(value: string) {
252
+ switch (value) {
253
+ case 'THUMBS_DOWN':
254
+ return '-1'
255
+ case 'THUMBS_UP':
256
+ return '+1'
257
+ default:
258
+ return value.toLowerCase()
259
+ }
260
+ }
261
+
262
+ function toReviewSummary(
263
+ item: Record<string, unknown>,
264
+ ): PullRequestReviewSummary {
265
+ return {
266
+ id: asNumericId(item.id ?? item.fullDatabaseId),
267
+ body: asString(item.body),
268
+ state: asString(item.state),
269
+ submittedAt: asNullableString(item.submitted_at ?? item.submittedAt),
270
+ url: asString(item.html_url ?? item.url),
271
+ userLogin: asString(
272
+ (item.user as LoginLike | null)?.login ??
273
+ (item.author as LoginLike | null)?.login,
274
+ ),
275
+ }
276
+ }
277
+
278
+ function toReviewThreadComment(
279
+ item: Record<string, unknown>,
280
+ ): PullRequestReviewThreadComment {
281
+ return {
282
+ body: asString(item.body),
283
+ createdAt: asString(item.createdAt),
284
+ line: asNullableNumber(item.line),
285
+ path: asString(item.path),
286
+ url: asString(item.url),
287
+ userLogin: asString((item.author as LoginLike | null)?.login),
288
+ }
289
+ }
290
+
291
+ function toReviewThreadPageNode(
292
+ item: Record<string, unknown>,
293
+ ): ReviewThreadPageNode {
294
+ const commentsConnection = requireConnectionRecord(
295
+ item.comments,
296
+ 'reviewThreads.comments',
297
+ )
298
+ return {
299
+ id: asString(item.id),
300
+ commentsPageInfo: commentsConnection.pageInfo,
301
+ isOutdated: asBoolean(item.isOutdated),
302
+ isResolved: asBoolean(item.isResolved),
303
+ comments: commentsConnection.nodes.map((comment) =>
304
+ toReviewThreadComment(comment),
305
+ ),
306
+ }
307
+ }
@@ -0,0 +1,137 @@
1
+ export interface BuildReviewThreadCommentsArgsInput {
2
+ after?: null | string
3
+ threadId: string
4
+ }
5
+
6
+ export interface BuildSnapshotArgsInput {
7
+ commentsAfter?: null | string
8
+ filesAfter?: null | string
9
+ includeComments: boolean
10
+ includeFiles: boolean
11
+ includeReactions: boolean
12
+ includeReviews: boolean
13
+ includeReviewThreads: boolean
14
+ number: number
15
+ owner: string
16
+ reactionsAfter?: null | string
17
+ repo: string
18
+ reviewsAfter?: null | string
19
+ reviewThreadsAfter?: null | string
20
+ }
21
+
22
+ export function buildReviewThreadCommentsArgs(
23
+ input: BuildReviewThreadCommentsArgsInput,
24
+ ) {
25
+ const args = ['api', 'graphql', '-F', `threadId=${input.threadId}`]
26
+ if (input.after) {
27
+ args.push('-f', `commentsAfter=${input.after}`)
28
+ }
29
+ args.push(
30
+ '-f',
31
+ `query=${[
32
+ 'query($threadId: ID!, $commentsAfter: String) {',
33
+ ' node(id: $threadId) {',
34
+ ' ... on PullRequestReviewThread {',
35
+ ' comments(first: 100, after: $commentsAfter) {',
36
+ ' pageInfo { hasNextPage endCursor }',
37
+ ' nodes { author { login } body createdAt path line url }',
38
+ ' }',
39
+ ' }',
40
+ ' }',
41
+ '}',
42
+ ].join('\n')}`,
43
+ )
44
+ return args
45
+ }
46
+
47
+ export function buildSnapshotArgs(input: BuildSnapshotArgsInput) {
48
+ const args = [
49
+ 'api',
50
+ 'graphql',
51
+ '-f',
52
+ `owner=${input.owner}`,
53
+ '-f',
54
+ `repo=${input.repo}`,
55
+ '-F',
56
+ `number=${input.number}`,
57
+ '-F',
58
+ `includeFiles=${String(input.includeFiles)}`,
59
+ '-F',
60
+ `includeReactions=${String(input.includeReactions)}`,
61
+ '-F',
62
+ `includeReviews=${String(input.includeReviews)}`,
63
+ '-F',
64
+ `includeComments=${String(input.includeComments)}`,
65
+ '-F',
66
+ `includeReviewThreads=${String(input.includeReviewThreads)}`,
67
+ ]
68
+ if (input.filesAfter) {
69
+ args.push('-f', `filesAfter=${input.filesAfter}`)
70
+ }
71
+ if (input.reactionsAfter) {
72
+ args.push('-f', `reactionsAfter=${input.reactionsAfter}`)
73
+ }
74
+ if (input.reviewsAfter) {
75
+ args.push('-f', `reviewsAfter=${input.reviewsAfter}`)
76
+ }
77
+ if (input.commentsAfter) {
78
+ args.push('-f', `commentsAfter=${input.commentsAfter}`)
79
+ }
80
+ if (input.reviewThreadsAfter) {
81
+ args.push('-f', `reviewThreadsAfter=${input.reviewThreadsAfter}`)
82
+ }
83
+ args.push(
84
+ '-f',
85
+ `query=${[
86
+ 'query(',
87
+ ' $owner: String!,',
88
+ ' $repo: String!,',
89
+ ' $number: Int!,',
90
+ ' $includeFiles: Boolean!,',
91
+ ' $includeReactions: Boolean!,',
92
+ ' $includeReviews: Boolean!,',
93
+ ' $includeComments: Boolean!,',
94
+ ' $includeReviewThreads: Boolean!,',
95
+ ' $filesAfter: String,',
96
+ ' $reactionsAfter: String,',
97
+ ' $reviewsAfter: String,',
98
+ ' $commentsAfter: String,',
99
+ ' $reviewThreadsAfter: String',
100
+ ') {',
101
+ ' repository(owner: $owner, name: $repo) {',
102
+ ' pullRequest(number: $number) {',
103
+ ' files(first: 100, after: $filesAfter) @include(if: $includeFiles) {',
104
+ ' pageInfo { hasNextPage endCursor }',
105
+ ' nodes { path }',
106
+ ' }',
107
+ ' reactions(first: 100, after: $reactionsAfter) @include(if: $includeReactions) {',
108
+ ' pageInfo { hasNextPage endCursor }',
109
+ ' nodes { content createdAt user { login } }',
110
+ ' }',
111
+ ' reviews(first: 100, after: $reviewsAfter) @include(if: $includeReviews) {',
112
+ ' pageInfo { hasNextPage endCursor }',
113
+ ' nodes { author { login } body fullDatabaseId state submittedAt url }',
114
+ ' }',
115
+ ' comments(first: 100, after: $commentsAfter) @include(if: $includeComments) {',
116
+ ' pageInfo { hasNextPage endCursor }',
117
+ ' nodes { author { login } body createdAt databaseId url }',
118
+ ' }',
119
+ ' reviewThreads(first: 100, after: $reviewThreadsAfter) @include(if: $includeReviewThreads) {',
120
+ ' pageInfo { hasNextPage endCursor }',
121
+ ' nodes {',
122
+ ' id',
123
+ ' isResolved',
124
+ ' isOutdated',
125
+ ' comments(first: 100) {',
126
+ ' pageInfo { hasNextPage endCursor }',
127
+ ' nodes { author { login } body createdAt path line url }',
128
+ ' }',
129
+ ' }',
130
+ ' }',
131
+ ' }',
132
+ ' }',
133
+ '}',
134
+ ].join('\n')}`,
135
+ )
136
+ return args
137
+ }