@llm4ts/flow 0.7.5 → 0.8.0
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/dist/BasecampTool.d.ts +75 -0
- package/dist/BasecampTool.d.ts.map +1 -0
- package/dist/BasecampTool.js +222 -0
- package/dist/BasecampTool.js.map +1 -0
- package/dist/BenchReport.d.ts +2 -2
- package/dist/BenchReport.d.ts.map +1 -1
- package/dist/CostLedger.d.ts +2 -2
- package/dist/CostLedger.d.ts.map +1 -1
- package/dist/Equiv.d.ts +3 -3
- package/dist/Equiv.d.ts.map +1 -1
- package/dist/Flow.d.ts +1 -1
- package/dist/Flow.d.ts.map +1 -1
- package/dist/FlowError.d.ts +9 -2
- package/dist/FlowError.d.ts.map +1 -1
- package/dist/FlowError.js +9 -0
- package/dist/FlowError.js.map +1 -1
- package/dist/GitHubTool.d.ts +10 -0
- package/dist/GitHubTool.d.ts.map +1 -1
- package/dist/GitHubTool.js +34 -0
- package/dist/GitHubTool.js.map +1 -1
- package/dist/Persistence.d.ts +2 -2
- package/dist/Persistence.d.ts.map +1 -1
- package/dist/PlanExecution.d.ts +1 -1
- package/dist/PlanExecution.d.ts.map +1 -1
- package/dist/Replay.d.ts +2 -2
- package/dist/Replay.d.ts.map +1 -1
- package/dist/Review.d.ts +2 -2
- package/dist/Review.d.ts.map +1 -1
- package/dist/ReviewCache.d.ts +1 -1
- package/dist/ReviewCache.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/BasecampTool.ts +413 -0
- package/src/FlowError.ts +10 -0
- package/src/GitHubTool.ts +60 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Ref from "effect/Ref"
|
|
3
|
+
import * as Schema from "effect/Schema"
|
|
4
|
+
import { Capabilities } from "@llm4ts/core/Capability"
|
|
5
|
+
import type { ProcessExecutorShape, ProcessResult } from "@llm4ts/core/ProcessExecutor"
|
|
6
|
+
import { ColumnNotFound, ProcessError, type FlowError } from "./FlowError.ts"
|
|
7
|
+
import type { FlowEventsShape } from "./FlowEvents.ts"
|
|
8
|
+
import { guarded } from "./CapabilityGuard.ts"
|
|
9
|
+
|
|
10
|
+
// One tool instance operates one board. `cardTable` is only needed when the
|
|
11
|
+
// project has more than one card table (the CLI resolves the single one).
|
|
12
|
+
export class BasecampProjectRef extends Schema.Class<BasecampProjectRef>("BasecampProjectRef")({
|
|
13
|
+
project: Schema.String,
|
|
14
|
+
cardTable: Schema.optionalKey(Schema.String)
|
|
15
|
+
}) {}
|
|
16
|
+
|
|
17
|
+
export class Column extends Schema.Class<Column>("Column")({
|
|
18
|
+
id: Schema.Int,
|
|
19
|
+
title: Schema.String
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
// Basecamp bodies are rich-text HTML, never Markdown — the field names say so.
|
|
23
|
+
export class Card extends Schema.Class<Card>("Card")({
|
|
24
|
+
id: Schema.Int,
|
|
25
|
+
title: Schema.String,
|
|
26
|
+
contentHtml: Schema.String,
|
|
27
|
+
column: Column,
|
|
28
|
+
assignees: Schema.Array(Schema.String),
|
|
29
|
+
commentsCount: Schema.Int,
|
|
30
|
+
updatedAt: Schema.String,
|
|
31
|
+
url: Schema.String
|
|
32
|
+
}) {}
|
|
33
|
+
|
|
34
|
+
export class CardComment extends Schema.Class<CardComment>("CardComment")({
|
|
35
|
+
id: Schema.Int,
|
|
36
|
+
author: Schema.String,
|
|
37
|
+
contentHtml: Schema.String,
|
|
38
|
+
createdAt: Schema.String
|
|
39
|
+
}) {}
|
|
40
|
+
|
|
41
|
+
export class CardStep extends Schema.Class<CardStep>("CardStep")({
|
|
42
|
+
id: Schema.Int,
|
|
43
|
+
title: Schema.String,
|
|
44
|
+
completed: Schema.Boolean
|
|
45
|
+
}) {}
|
|
46
|
+
|
|
47
|
+
const projectFlags = (board: BasecampProjectRef): ReadonlyArray<string> => [
|
|
48
|
+
"--project",
|
|
49
|
+
board.project,
|
|
50
|
+
...(board.cardTable === undefined ? [] : ["--card-table", board.cardTable])
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const jsonFlags = ["--json", "--quiet"]
|
|
54
|
+
|
|
55
|
+
export const cardColumnsArgs = (board: BasecampProjectRef): ReadonlyArray<string> => [
|
|
56
|
+
"cards",
|
|
57
|
+
"columns",
|
|
58
|
+
...projectFlags(board),
|
|
59
|
+
...jsonFlags
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
export const cardListArgs = (board: BasecampProjectRef, column: Column): ReadonlyArray<string> => [
|
|
63
|
+
"cards",
|
|
64
|
+
"list",
|
|
65
|
+
"--column",
|
|
66
|
+
String(column.id),
|
|
67
|
+
"--all",
|
|
68
|
+
...projectFlags(board),
|
|
69
|
+
...jsonFlags
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
export const cardShowArgs = (board: BasecampProjectRef, cardId: number): ReadonlyArray<string> => [
|
|
73
|
+
"cards",
|
|
74
|
+
"show",
|
|
75
|
+
String(cardId),
|
|
76
|
+
...projectFlags(board),
|
|
77
|
+
...jsonFlags
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
export const cardMoveArgs = (
|
|
81
|
+
board: BasecampProjectRef,
|
|
82
|
+
cardId: number,
|
|
83
|
+
column: Column
|
|
84
|
+
): ReadonlyArray<string> => [
|
|
85
|
+
"cards",
|
|
86
|
+
"move",
|
|
87
|
+
String(cardId),
|
|
88
|
+
"--to",
|
|
89
|
+
String(column.id),
|
|
90
|
+
...projectFlags(board),
|
|
91
|
+
"--quiet"
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
export const cardCreateArgs = (
|
|
95
|
+
board: BasecampProjectRef,
|
|
96
|
+
column: Column,
|
|
97
|
+
title: string,
|
|
98
|
+
content: string,
|
|
99
|
+
assignee?: string
|
|
100
|
+
): ReadonlyArray<string> => [
|
|
101
|
+
"cards",
|
|
102
|
+
"create",
|
|
103
|
+
title,
|
|
104
|
+
content,
|
|
105
|
+
"--column",
|
|
106
|
+
String(column.id),
|
|
107
|
+
...(assignee === undefined ? [] : ["--assignee", assignee]),
|
|
108
|
+
...projectFlags(board),
|
|
109
|
+
...jsonFlags
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
export const cardAssignArgs = (
|
|
113
|
+
board: BasecampProjectRef,
|
|
114
|
+
cardId: number,
|
|
115
|
+
assignee: string
|
|
116
|
+
): ReadonlyArray<string> => [
|
|
117
|
+
"cards",
|
|
118
|
+
"update",
|
|
119
|
+
String(cardId),
|
|
120
|
+
"--assignee",
|
|
121
|
+
assignee,
|
|
122
|
+
...projectFlags(board),
|
|
123
|
+
"--quiet"
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
// Comments hang off the card alone; `basecamp comments` rejects --project.
|
|
127
|
+
export const cardCommentsArgs = (cardId: number): ReadonlyArray<string> => [
|
|
128
|
+
"comments",
|
|
129
|
+
"list",
|
|
130
|
+
String(cardId),
|
|
131
|
+
"--all",
|
|
132
|
+
...jsonFlags
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
export const cardCommentCreateArgs = (cardId: number, body: string): ReadonlyArray<string> => [
|
|
136
|
+
"comments",
|
|
137
|
+
"create",
|
|
138
|
+
String(cardId),
|
|
139
|
+
body,
|
|
140
|
+
"--quiet"
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
export const cardStepsArgs = (board: BasecampProjectRef, cardId: number): ReadonlyArray<string> => [
|
|
144
|
+
"cards",
|
|
145
|
+
"steps",
|
|
146
|
+
String(cardId),
|
|
147
|
+
...projectFlags(board),
|
|
148
|
+
...jsonFlags
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
export const cardStepCompleteArgs = (
|
|
152
|
+
board: BasecampProjectRef,
|
|
153
|
+
stepId: number
|
|
154
|
+
): ReadonlyArray<string> => [
|
|
155
|
+
"cards",
|
|
156
|
+
"step",
|
|
157
|
+
"complete",
|
|
158
|
+
String(stepId),
|
|
159
|
+
...projectFlags(board),
|
|
160
|
+
"--quiet"
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
const decodeJson = <A, S extends Schema.Codec<A, string>>(
|
|
164
|
+
operation: string,
|
|
165
|
+
schema: S,
|
|
166
|
+
json: string
|
|
167
|
+
): Effect.Effect<S["Type"], ProcessError> =>
|
|
168
|
+
Schema.decodeUnknownEffect(schema)(json).pipe(
|
|
169
|
+
Effect.mapError((error) =>
|
|
170
|
+
ProcessError.make({
|
|
171
|
+
message: operation,
|
|
172
|
+
detail: String(error)
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const columnStruct = Schema.Struct({
|
|
178
|
+
id: Schema.Int,
|
|
179
|
+
title: Schema.String
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
export const parseColumns = (json: string): Effect.Effect<ReadonlyArray<Column>, ProcessError> =>
|
|
183
|
+
decodeJson(
|
|
184
|
+
"basecamp parse columns",
|
|
185
|
+
Schema.fromJsonString(Schema.Array(columnStruct)),
|
|
186
|
+
json
|
|
187
|
+
).pipe(Effect.map((columns) => columns.map((column) => Column.make(column))))
|
|
188
|
+
|
|
189
|
+
const cardStruct = Schema.Struct({
|
|
190
|
+
id: Schema.Int,
|
|
191
|
+
title: Schema.String,
|
|
192
|
+
content: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
193
|
+
parent: columnStruct,
|
|
194
|
+
assignees: Schema.optionalKey(
|
|
195
|
+
Schema.NullOr(Schema.Array(Schema.Struct({ name: Schema.String })))
|
|
196
|
+
),
|
|
197
|
+
comments_count: Schema.optionalKey(Schema.NullOr(Schema.Int)),
|
|
198
|
+
updated_at: Schema.String,
|
|
199
|
+
app_url: Schema.String
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const toCard = (item: typeof cardStruct.Type): Card =>
|
|
203
|
+
Card.make({
|
|
204
|
+
id: item.id,
|
|
205
|
+
title: item.title,
|
|
206
|
+
contentHtml: item.content ?? "",
|
|
207
|
+
column: Column.make(item.parent),
|
|
208
|
+
assignees: (item.assignees ?? []).map((assignee) => assignee.name),
|
|
209
|
+
commentsCount: item.comments_count ?? 0,
|
|
210
|
+
updatedAt: item.updated_at,
|
|
211
|
+
url: item.app_url
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
export const parseCard = (json: string): Effect.Effect<Card, ProcessError> =>
|
|
215
|
+
decodeJson("basecamp parse card", Schema.fromJsonString(cardStruct), json).pipe(
|
|
216
|
+
Effect.map(toCard)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
export const parseCards = (json: string): Effect.Effect<ReadonlyArray<Card>, ProcessError> =>
|
|
220
|
+
decodeJson("basecamp parse cards", Schema.fromJsonString(Schema.Array(cardStruct)), json).pipe(
|
|
221
|
+
Effect.map((items) => items.map(toCard))
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const commentStruct = Schema.Struct({
|
|
225
|
+
id: Schema.Int,
|
|
226
|
+
content: Schema.String,
|
|
227
|
+
creator: Schema.Struct({ name: Schema.String }),
|
|
228
|
+
created_at: Schema.String
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
export const parseCardComments = (
|
|
232
|
+
json: string
|
|
233
|
+
): Effect.Effect<ReadonlyArray<CardComment>, ProcessError> =>
|
|
234
|
+
decodeJson(
|
|
235
|
+
"basecamp parse comments",
|
|
236
|
+
Schema.fromJsonString(Schema.Array(commentStruct)),
|
|
237
|
+
json
|
|
238
|
+
).pipe(
|
|
239
|
+
Effect.map((comments) =>
|
|
240
|
+
comments.map((comment) =>
|
|
241
|
+
CardComment.make({
|
|
242
|
+
id: comment.id,
|
|
243
|
+
author: comment.creator.name,
|
|
244
|
+
contentHtml: comment.content,
|
|
245
|
+
createdAt: comment.created_at
|
|
246
|
+
})
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const stepStruct = Schema.Struct({
|
|
252
|
+
id: Schema.Int,
|
|
253
|
+
title: Schema.String,
|
|
254
|
+
completed: Schema.Boolean
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// A card without steps prints `null`, not `[]`.
|
|
258
|
+
export const parseCardSteps = (
|
|
259
|
+
json: string
|
|
260
|
+
): Effect.Effect<ReadonlyArray<CardStep>, ProcessError> =>
|
|
261
|
+
decodeJson(
|
|
262
|
+
"basecamp parse steps",
|
|
263
|
+
Schema.fromJsonString(Schema.NullOr(Schema.Array(stepStruct))),
|
|
264
|
+
json
|
|
265
|
+
).pipe(Effect.map((steps) => (steps ?? []).map((step) => CardStep.make(step))))
|
|
266
|
+
|
|
267
|
+
export interface BasecampToolShape {
|
|
268
|
+
readonly listColumns: Effect.Effect<ReadonlyArray<Column>, FlowError>
|
|
269
|
+
readonly resolveColumn: (title: string) => Effect.Effect<Column, FlowError>
|
|
270
|
+
readonly listCards: (column: Column) => Effect.Effect<ReadonlyArray<Card>, FlowError>
|
|
271
|
+
readonly readCard: (cardId: number) => Effect.Effect<Card, FlowError>
|
|
272
|
+
readonly moveCard: (cardId: number, column: Column) => Effect.Effect<void, FlowError>
|
|
273
|
+
readonly createCard: (
|
|
274
|
+
column: Column,
|
|
275
|
+
title: string,
|
|
276
|
+
content: string,
|
|
277
|
+
assignee?: string
|
|
278
|
+
) => Effect.Effect<Card, FlowError>
|
|
279
|
+
readonly assignCard: (cardId: number, assignee: string) => Effect.Effect<void, FlowError>
|
|
280
|
+
readonly readCardComments: (
|
|
281
|
+
cardId: number
|
|
282
|
+
) => Effect.Effect<ReadonlyArray<CardComment>, FlowError>
|
|
283
|
+
readonly writeCardComment: (cardId: number, body: string) => Effect.Effect<void, FlowError>
|
|
284
|
+
readonly listSteps: (cardId: number) => Effect.Effect<ReadonlyArray<CardStep>, FlowError>
|
|
285
|
+
readonly completeStep: (stepId: number) => Effect.Effect<void, FlowError>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const output = (result: ProcessResult): string => result.stdout.join("\n").trim()
|
|
289
|
+
|
|
290
|
+
export const makeBasecampTool = Effect.fn("@llm4ts/flow/BasecampTool.make")(function* (
|
|
291
|
+
process: ProcessExecutorShape,
|
|
292
|
+
workDir: string,
|
|
293
|
+
events: FlowEventsShape,
|
|
294
|
+
board: BasecampProjectRef
|
|
295
|
+
): Effect.fn.Return<BasecampToolShape> {
|
|
296
|
+
const columnsCache = yield* Ref.make<ReadonlyArray<Column> | undefined>(undefined)
|
|
297
|
+
|
|
298
|
+
const run = (args: ReadonlyArray<string>): Effect.Effect<ProcessResult, FlowError> =>
|
|
299
|
+
process.run(["basecamp", ...args], workDir, {}).pipe(
|
|
300
|
+
Effect.mapError((error) =>
|
|
301
|
+
ProcessError.make({
|
|
302
|
+
message: `basecamp ${args.join(" ")}`,
|
|
303
|
+
detail: error.message
|
|
304
|
+
})
|
|
305
|
+
),
|
|
306
|
+
Effect.flatMap((result) =>
|
|
307
|
+
result.exitCode === 0
|
|
308
|
+
? Effect.succeed(result)
|
|
309
|
+
: Effect.fail(
|
|
310
|
+
ProcessError.make({
|
|
311
|
+
message: `basecamp ${args.join(" ")}`,
|
|
312
|
+
detail:
|
|
313
|
+
[...result.stdout, ...result.stderr].join("\n").trim() ||
|
|
314
|
+
`exit code ${result.exitCode}`
|
|
315
|
+
})
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
const read = <A>(
|
|
321
|
+
operation: string,
|
|
322
|
+
effect: Effect.Effect<A, FlowError>
|
|
323
|
+
): Effect.Effect<A, FlowError> => guarded(Capabilities.BasecampRead, operation, events, effect)
|
|
324
|
+
const write = <A>(
|
|
325
|
+
operation: string,
|
|
326
|
+
effect: Effect.Effect<A, FlowError>
|
|
327
|
+
): Effect.Effect<A, FlowError> => guarded(Capabilities.BasecampWrite, operation, events, effect)
|
|
328
|
+
|
|
329
|
+
// The board's columns barely change within a tool's lifetime; fetch once.
|
|
330
|
+
const cachedColumns: Effect.Effect<ReadonlyArray<Column>, FlowError> = Ref.get(columnsCache).pipe(
|
|
331
|
+
Effect.flatMap((cached) =>
|
|
332
|
+
cached !== undefined
|
|
333
|
+
? Effect.succeed(cached)
|
|
334
|
+
: run(cardColumnsArgs(board)).pipe(
|
|
335
|
+
Effect.flatMap((result) => parseColumns(output(result))),
|
|
336
|
+
Effect.tap((columns) => Ref.set(columnsCache, columns))
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
listColumns: read("basecamp cards columns", cachedColumns),
|
|
343
|
+
resolveColumn: (title) =>
|
|
344
|
+
read(
|
|
345
|
+
"basecamp cards columns",
|
|
346
|
+
cachedColumns.pipe(
|
|
347
|
+
Effect.flatMap((columns) => {
|
|
348
|
+
const match = columns.find(
|
|
349
|
+
(column) => column.title.toLowerCase() === title.toLowerCase()
|
|
350
|
+
)
|
|
351
|
+
return match !== undefined
|
|
352
|
+
? Effect.succeed(match)
|
|
353
|
+
: Effect.fail(
|
|
354
|
+
ColumnNotFound.make({
|
|
355
|
+
title,
|
|
356
|
+
available: columns.map((column) => column.title)
|
|
357
|
+
})
|
|
358
|
+
)
|
|
359
|
+
})
|
|
360
|
+
)
|
|
361
|
+
),
|
|
362
|
+
listCards: (column) =>
|
|
363
|
+
read(
|
|
364
|
+
"basecamp cards list",
|
|
365
|
+
run(cardListArgs(board, column)).pipe(
|
|
366
|
+
Effect.flatMap((result) => parseCards(output(result)))
|
|
367
|
+
)
|
|
368
|
+
),
|
|
369
|
+
readCard: (cardId) =>
|
|
370
|
+
read(
|
|
371
|
+
"basecamp cards show",
|
|
372
|
+
run(cardShowArgs(board, cardId)).pipe(Effect.flatMap((result) => parseCard(output(result))))
|
|
373
|
+
),
|
|
374
|
+
moveCard: (cardId, column) =>
|
|
375
|
+
write("basecamp cards move", run(cardMoveArgs(board, cardId, column)).pipe(Effect.asVoid)),
|
|
376
|
+
createCard: (column, title, content, assignee) =>
|
|
377
|
+
write(
|
|
378
|
+
"basecamp cards create",
|
|
379
|
+
run(cardCreateArgs(board, column, title, content, assignee)).pipe(
|
|
380
|
+
Effect.flatMap((result) => parseCard(output(result)))
|
|
381
|
+
)
|
|
382
|
+
),
|
|
383
|
+
assignCard: (cardId, assignee) =>
|
|
384
|
+
write(
|
|
385
|
+
"basecamp cards update",
|
|
386
|
+
run(cardAssignArgs(board, cardId, assignee)).pipe(Effect.asVoid)
|
|
387
|
+
),
|
|
388
|
+
readCardComments: (cardId) =>
|
|
389
|
+
read(
|
|
390
|
+
"basecamp comments list",
|
|
391
|
+
run(cardCommentsArgs(cardId)).pipe(
|
|
392
|
+
Effect.flatMap((result) => parseCardComments(output(result)))
|
|
393
|
+
)
|
|
394
|
+
),
|
|
395
|
+
writeCardComment: (cardId, body) =>
|
|
396
|
+
write(
|
|
397
|
+
"basecamp comments create",
|
|
398
|
+
run(cardCommentCreateArgs(cardId, body)).pipe(Effect.asVoid)
|
|
399
|
+
),
|
|
400
|
+
listSteps: (cardId) =>
|
|
401
|
+
read(
|
|
402
|
+
"basecamp cards steps",
|
|
403
|
+
run(cardStepsArgs(board, cardId)).pipe(
|
|
404
|
+
Effect.flatMap((result) => parseCardSteps(output(result)))
|
|
405
|
+
)
|
|
406
|
+
),
|
|
407
|
+
completeStep: (stepId) =>
|
|
408
|
+
write(
|
|
409
|
+
"basecamp cards step complete",
|
|
410
|
+
run(cardStepCompleteArgs(board, stepId)).pipe(Effect.asVoid)
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
})
|
package/src/FlowError.ts
CHANGED
|
@@ -105,6 +105,15 @@ export class FlowCapabilityDenied extends Schema.TaggedErrorClass<FlowCapability
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export class ColumnNotFound extends Schema.TaggedErrorClass<ColumnNotFound>()("ColumnNotFound", {
|
|
109
|
+
title: Schema.String,
|
|
110
|
+
available: Schema.Array(Schema.String)
|
|
111
|
+
}) {
|
|
112
|
+
get message(): string {
|
|
113
|
+
return `column "${this.title}" not found on the card table; available: ${this.available.join(", ")}`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
export class BudgetExceeded extends Schema.TaggedErrorClass<BudgetExceeded>()("BudgetExceeded", {
|
|
109
118
|
metric: Schema.Literals(["tokens", "costUsd"]),
|
|
110
119
|
limit: Schema.Number,
|
|
@@ -126,6 +135,7 @@ export const FlowError = Schema.Union([
|
|
|
126
135
|
ProcessError,
|
|
127
136
|
FlowLlmError,
|
|
128
137
|
FlowCapabilityDenied,
|
|
138
|
+
ColumnNotFound,
|
|
129
139
|
BudgetExceeded
|
|
130
140
|
])
|
|
131
141
|
export type FlowError = typeof FlowError.Type
|
package/src/GitHubTool.ts
CHANGED
|
@@ -153,6 +153,22 @@ export const issueCommentEditArgs = (
|
|
|
153
153
|
`body=${body}`
|
|
154
154
|
]
|
|
155
155
|
|
|
156
|
+
export class IssueComment extends Schema.Class<IssueComment>("IssueComment")({
|
|
157
|
+
author: Schema.String,
|
|
158
|
+
body: Schema.String,
|
|
159
|
+
createdAt: Schema.String
|
|
160
|
+
}) {}
|
|
161
|
+
|
|
162
|
+
export const issueCommentsArgs = (ref: IssueRef): ReadonlyArray<string> => [
|
|
163
|
+
"issue",
|
|
164
|
+
"view",
|
|
165
|
+
String(ref.number),
|
|
166
|
+
"--repo",
|
|
167
|
+
`${ref.owner}/${ref.repo}`,
|
|
168
|
+
"--json",
|
|
169
|
+
"comments"
|
|
170
|
+
]
|
|
171
|
+
|
|
156
172
|
export const issueCommentArgs = (ref: IssueRef, body: string): ReadonlyArray<string> => [
|
|
157
173
|
"issue",
|
|
158
174
|
"comment",
|
|
@@ -317,6 +333,18 @@ class GhIssueSummary extends Schema.Class<GhIssueSummary>("GhIssueSummary")({
|
|
|
317
333
|
updatedAt: Schema.String
|
|
318
334
|
}) {}
|
|
319
335
|
|
|
336
|
+
class GhComment extends Schema.Class<GhComment>("GhComment")({
|
|
337
|
+
author: GhAuthor,
|
|
338
|
+
body: Schema.String,
|
|
339
|
+
createdAt: Schema.String
|
|
340
|
+
}) {}
|
|
341
|
+
|
|
342
|
+
class GhComments extends Schema.Class<GhComments>("GhComments")({
|
|
343
|
+
comments: Schema.Array(GhComment).pipe(
|
|
344
|
+
Schema.withConstructorDefault(Effect.succeed(Object.freeze([])))
|
|
345
|
+
)
|
|
346
|
+
}) {}
|
|
347
|
+
|
|
320
348
|
class GhCheck extends Schema.Class<GhCheck>("GhCheck")({
|
|
321
349
|
status: Schema.optionalKey(Schema.String),
|
|
322
350
|
conclusion: Schema.optionalKey(Schema.String),
|
|
@@ -372,6 +400,28 @@ export const parseIssueList = (
|
|
|
372
400
|
)
|
|
373
401
|
)
|
|
374
402
|
|
|
403
|
+
export const parseIssueComments = (
|
|
404
|
+
json: string
|
|
405
|
+
): Effect.Effect<ReadonlyArray<IssueComment>, ProcessError> =>
|
|
406
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(GhComments))(json).pipe(
|
|
407
|
+
Effect.map((parsed) =>
|
|
408
|
+
parsed.comments.map(
|
|
409
|
+
(comment) =>
|
|
410
|
+
new IssueComment({
|
|
411
|
+
author: comment.author.login,
|
|
412
|
+
body: comment.body,
|
|
413
|
+
createdAt: comment.createdAt
|
|
414
|
+
})
|
|
415
|
+
)
|
|
416
|
+
),
|
|
417
|
+
Effect.mapError((error) =>
|
|
418
|
+
ProcessError.make({
|
|
419
|
+
message: "gh issue view comments",
|
|
420
|
+
detail: `invalid comments JSON: ${String(error)}`
|
|
421
|
+
})
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
375
425
|
export const outcomeFromChecksJson = (json: string): Effect.Effect<BuildOutcome, ProcessError> =>
|
|
376
426
|
Schema.decodeUnknownEffect(Schema.fromJsonString(GhChecks))(json).pipe(
|
|
377
427
|
Effect.map((parsed) => {
|
|
@@ -404,6 +454,9 @@ export interface GitHubToolShape {
|
|
|
404
454
|
draft?: boolean
|
|
405
455
|
) => Effect.Effect<PullRequest, FlowError>
|
|
406
456
|
readonly readIssue: (ref: IssueRef) => Effect.Effect<Issue, FlowError>
|
|
457
|
+
readonly readIssueComments: (
|
|
458
|
+
ref: IssueRef
|
|
459
|
+
) => Effect.Effect<ReadonlyArray<IssueComment>, FlowError>
|
|
407
460
|
// Returns the created comment's reference when the gh output carries it
|
|
408
461
|
// (undefined otherwise), so callers can edit the comment later — e.g. a
|
|
409
462
|
// plan checklist kept up to date as tasks complete.
|
|
@@ -531,6 +584,13 @@ export const makeGitHubTool = (
|
|
|
531
584
|
"gh issue view",
|
|
532
585
|
run(issueViewArgs(ref)).pipe(Effect.flatMap((result) => parseIssue(output(result))))
|
|
533
586
|
),
|
|
587
|
+
readIssueComments: (ref) =>
|
|
588
|
+
read(
|
|
589
|
+
"gh issue view comments",
|
|
590
|
+
run(issueCommentsArgs(ref)).pipe(
|
|
591
|
+
Effect.flatMap((result) => parseIssueComments(output(result)))
|
|
592
|
+
)
|
|
593
|
+
),
|
|
534
594
|
writeIssueComment: (ref, body) =>
|
|
535
595
|
write(
|
|
536
596
|
"gh issue comment",
|