@llm4ts/flow 0.1.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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/dist/AzureDevOpsTool.d.ts +60 -0
  4. package/dist/AzureDevOpsTool.d.ts.map +1 -0
  5. package/dist/AzureDevOpsTool.js +163 -0
  6. package/dist/AzureDevOpsTool.js.map +1 -0
  7. package/dist/Bench.d.ts +133 -0
  8. package/dist/Bench.d.ts.map +1 -0
  9. package/dist/Bench.js +272 -0
  10. package/dist/Bench.js.map +1 -0
  11. package/dist/BenchReport.d.ts +9 -0
  12. package/dist/BenchReport.d.ts.map +1 -0
  13. package/dist/BenchReport.js +360 -0
  14. package/dist/BenchReport.js.map +1 -0
  15. package/dist/CapabilityGuard.d.ts +6 -0
  16. package/dist/CapabilityGuard.d.ts.map +1 -0
  17. package/dist/CapabilityGuard.js +25 -0
  18. package/dist/CapabilityGuard.js.map +1 -0
  19. package/dist/Chat.d.ts +15 -0
  20. package/dist/Chat.d.ts.map +1 -0
  21. package/dist/Chat.js +36 -0
  22. package/dist/Chat.js.map +1 -0
  23. package/dist/Classified.d.ts +14 -0
  24. package/dist/Classified.d.ts.map +1 -0
  25. package/dist/Classified.js +48 -0
  26. package/dist/Classified.js.map +1 -0
  27. package/dist/CostLedger.d.ts +55 -0
  28. package/dist/CostLedger.d.ts.map +1 -0
  29. package/dist/CostLedger.js +126 -0
  30. package/dist/CostLedger.js.map +1 -0
  31. package/dist/CostTracker.d.ts +13 -0
  32. package/dist/CostTracker.d.ts.map +1 -0
  33. package/dist/CostTracker.js +116 -0
  34. package/dist/CostTracker.js.map +1 -0
  35. package/dist/Equiv.d.ts +163 -0
  36. package/dist/Equiv.d.ts.map +1 -0
  37. package/dist/Equiv.js +256 -0
  38. package/dist/Equiv.js.map +1 -0
  39. package/dist/EquivReport.d.ts +16 -0
  40. package/dist/EquivReport.d.ts.map +1 -0
  41. package/dist/EquivReport.js +77 -0
  42. package/dist/EquivReport.js.map +1 -0
  43. package/dist/Flow.d.ts +25 -0
  44. package/dist/Flow.d.ts.map +1 -0
  45. package/dist/Flow.js +39 -0
  46. package/dist/Flow.js.map +1 -0
  47. package/dist/FlowContext.d.ts +23 -0
  48. package/dist/FlowContext.d.ts.map +1 -0
  49. package/dist/FlowContext.js +4 -0
  50. package/dist/FlowContext.js.map +1 -0
  51. package/dist/FlowError.d.ts +80 -0
  52. package/dist/FlowError.d.ts.map +1 -0
  53. package/dist/FlowError.js +92 -0
  54. package/dist/FlowError.js.map +1 -0
  55. package/dist/FlowEvents.d.ts +101 -0
  56. package/dist/FlowEvents.d.ts.map +1 -0
  57. package/dist/FlowEvents.js +103 -0
  58. package/dist/FlowEvents.js.map +1 -0
  59. package/dist/FlowRecorder.d.ts +26 -0
  60. package/dist/FlowRecorder.d.ts.map +1 -0
  61. package/dist/FlowRecorder.js +74 -0
  62. package/dist/FlowRecorder.js.map +1 -0
  63. package/dist/GitHubTool.d.ts +53 -0
  64. package/dist/GitHubTool.d.ts.map +1 -0
  65. package/dist/GitHubTool.js +191 -0
  66. package/dist/GitHubTool.js.map +1 -0
  67. package/dist/GitTool.d.ts +46 -0
  68. package/dist/GitTool.d.ts.map +1 -0
  69. package/dist/GitTool.js +97 -0
  70. package/dist/GitTool.js.map +1 -0
  71. package/dist/Interaction.d.ts +21 -0
  72. package/dist/Interaction.d.ts.map +1 -0
  73. package/dist/Interaction.js +9 -0
  74. package/dist/Interaction.js.map +1 -0
  75. package/dist/McpServer.d.ts +18 -0
  76. package/dist/McpServer.d.ts.map +1 -0
  77. package/dist/McpServer.js +122 -0
  78. package/dist/McpServer.js.map +1 -0
  79. package/dist/Mermaid.d.ts +7 -0
  80. package/dist/Mermaid.d.ts.map +1 -0
  81. package/dist/Mermaid.js +98 -0
  82. package/dist/Mermaid.js.map +1 -0
  83. package/dist/Pack.d.ts +40 -0
  84. package/dist/Pack.d.ts.map +1 -0
  85. package/dist/Pack.js +141 -0
  86. package/dist/Pack.js.map +1 -0
  87. package/dist/Package.d.ts +2 -0
  88. package/dist/Package.d.ts.map +1 -0
  89. package/dist/Package.js +2 -0
  90. package/dist/Package.js.map +1 -0
  91. package/dist/Persistence.d.ts +32 -0
  92. package/dist/Persistence.d.ts.map +1 -0
  93. package/dist/Persistence.js +85 -0
  94. package/dist/Persistence.js.map +1 -0
  95. package/dist/Plan.d.ts +26 -0
  96. package/dist/Plan.d.ts.map +1 -0
  97. package/dist/Plan.js +101 -0
  98. package/dist/Plan.js.map +1 -0
  99. package/dist/PlanExecution.d.ts +7 -0
  100. package/dist/PlanExecution.d.ts.map +1 -0
  101. package/dist/PlanExecution.js +19 -0
  102. package/dist/PlanExecution.js.map +1 -0
  103. package/dist/Planner.d.ts +32 -0
  104. package/dist/Planner.d.ts.map +1 -0
  105. package/dist/Planner.js +97 -0
  106. package/dist/Planner.js.map +1 -0
  107. package/dist/PrSummary.d.ts +15 -0
  108. package/dist/PrSummary.d.ts.map +1 -0
  109. package/dist/PrSummary.js +29 -0
  110. package/dist/PrSummary.js.map +1 -0
  111. package/dist/PriceList.d.ts +11 -0
  112. package/dist/PriceList.d.ts.map +1 -0
  113. package/dist/PriceList.js +79 -0
  114. package/dist/PriceList.js.map +1 -0
  115. package/dist/Provenance.d.ts +27 -0
  116. package/dist/Provenance.d.ts.map +1 -0
  117. package/dist/Provenance.js +38 -0
  118. package/dist/Provenance.js.map +1 -0
  119. package/dist/Replay.d.ts +29 -0
  120. package/dist/Replay.d.ts.map +1 -0
  121. package/dist/Replay.js +181 -0
  122. package/dist/Replay.js.map +1 -0
  123. package/dist/Review.d.ts +71 -0
  124. package/dist/Review.d.ts.map +1 -0
  125. package/dist/Review.js +204 -0
  126. package/dist/Review.js.map +1 -0
  127. package/dist/ReviewCache.d.ts +6 -0
  128. package/dist/ReviewCache.d.ts.map +1 -0
  129. package/dist/ReviewCache.js +28 -0
  130. package/dist/ReviewCache.js.map +1 -0
  131. package/dist/Reviewer.d.ts +12 -0
  132. package/dist/Reviewer.d.ts.map +1 -0
  133. package/dist/Reviewer.js +30 -0
  134. package/dist/Reviewer.js.map +1 -0
  135. package/dist/SpecChecks.d.ts +17 -0
  136. package/dist/SpecChecks.d.ts.map +1 -0
  137. package/dist/SpecChecks.js +107 -0
  138. package/dist/SpecChecks.js.map +1 -0
  139. package/dist/Survey.d.ts +32 -0
  140. package/dist/Survey.d.ts.map +1 -0
  141. package/dist/Survey.js +119 -0
  142. package/dist/Survey.js.map +1 -0
  143. package/dist/TransientRetry.d.ts +16 -0
  144. package/dist/TransientRetry.d.ts.map +1 -0
  145. package/dist/TransientRetry.js +102 -0
  146. package/dist/TransientRetry.js.map +1 -0
  147. package/dist/Workspace.d.ts +39 -0
  148. package/dist/Workspace.d.ts.map +1 -0
  149. package/dist/Workspace.js +192 -0
  150. package/dist/Workspace.js.map +1 -0
  151. package/dist/WorkspaceLayout.d.ts +3 -0
  152. package/dist/WorkspaceLayout.d.ts.map +1 -0
  153. package/dist/WorkspaceLayout.js +19 -0
  154. package/dist/WorkspaceLayout.js.map +1 -0
  155. package/dist/WorkspaceTools.d.ts +4 -0
  156. package/dist/WorkspaceTools.d.ts.map +1 -0
  157. package/dist/WorkspaceTools.js +130 -0
  158. package/dist/WorkspaceTools.js.map +1 -0
  159. package/package.json +91 -0
  160. package/src/AzureDevOpsTool.ts +291 -0
  161. package/src/Bench.ts +340 -0
  162. package/src/BenchReport.ts +447 -0
  163. package/src/CapabilityGuard.ts +43 -0
  164. package/src/Chat.ts +60 -0
  165. package/src/Classified.ts +67 -0
  166. package/src/CostLedger.ts +171 -0
  167. package/src/CostTracker.ts +170 -0
  168. package/src/Equiv.ts +378 -0
  169. package/src/EquivReport.ts +92 -0
  170. package/src/Flow.ts +72 -0
  171. package/src/FlowContext.ts +23 -0
  172. package/src/FlowError.ts +112 -0
  173. package/src/FlowEvents.ts +147 -0
  174. package/src/FlowRecorder.ts +116 -0
  175. package/src/GitHubTool.ts +315 -0
  176. package/src/GitTool.ts +214 -0
  177. package/src/Interaction.ts +23 -0
  178. package/src/McpServer.ts +168 -0
  179. package/src/Mermaid.ts +112 -0
  180. package/src/Pack.ts +213 -0
  181. package/src/Package.ts +1 -0
  182. package/src/Persistence.ts +198 -0
  183. package/src/Plan.ts +119 -0
  184. package/src/PlanExecution.ts +46 -0
  185. package/src/Planner.ts +134 -0
  186. package/src/PrSummary.ts +37 -0
  187. package/src/PriceList.ts +91 -0
  188. package/src/Provenance.ts +85 -0
  189. package/src/Replay.ts +265 -0
  190. package/src/Review.ts +337 -0
  191. package/src/ReviewCache.ts +45 -0
  192. package/src/Reviewer.ts +31 -0
  193. package/src/SpecChecks.ts +143 -0
  194. package/src/Survey.ts +144 -0
  195. package/src/TransientRetry.ts +238 -0
  196. package/src/Workspace.ts +255 -0
  197. package/src/WorkspaceLayout.ts +22 -0
  198. package/src/WorkspaceTools.ts +188 -0
@@ -0,0 +1,147 @@
1
+ import * as Context from "effect/Context"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Layer from "effect/Layer"
4
+ import * as PubSub from "effect/PubSub"
5
+ import * as Ref from "effect/Ref"
6
+ import * as Schema from "effect/Schema"
7
+ import type * as Scope from "effect/Scope"
8
+ import * as Stream from "effect/Stream"
9
+ import { TokenUsage } from "@llm4ts/core/Models"
10
+
11
+ export class StageStarted extends Schema.TaggedClass<StageStarted>()("StageStarted", {
12
+ stage: Schema.String
13
+ }) {}
14
+
15
+ export class StageCompleted extends Schema.TaggedClass<StageCompleted>()("StageCompleted", {
16
+ stage: Schema.String
17
+ }) {}
18
+
19
+ export class StageFailed extends Schema.TaggedClass<StageFailed>()("StageFailed", {
20
+ stage: Schema.String,
21
+ message: Schema.String
22
+ }) {}
23
+
24
+ export class Aborted extends Schema.TaggedClass<Aborted>()("Aborted", {
25
+ message: Schema.String
26
+ }) {}
27
+
28
+ export class Info extends Schema.TaggedClass<Info>()("Info", {
29
+ message: Schema.String
30
+ }) {}
31
+
32
+ export class ToolUse extends Schema.TaggedClass<ToolUse>()("ToolUse", {
33
+ tool: Schema.String,
34
+ args: Schema.String
35
+ }) {}
36
+
37
+ export class AssistantMessage extends Schema.TaggedClass<AssistantMessage>()("AssistantMessage", {
38
+ text: Schema.String
39
+ }) {}
40
+
41
+ export class TokensUsed extends Schema.TaggedClass<TokensUsed>()("TokensUsed", {
42
+ agent: Schema.String,
43
+ model: Schema.optionalKey(Schema.String),
44
+ usage: TokenUsage
45
+ }) {}
46
+
47
+ export class CapabilityUsedEvent extends Schema.TaggedClass<CapabilityUsedEvent>()(
48
+ "CapabilityUsed",
49
+ {
50
+ capability: Schema.String,
51
+ operation: Schema.String
52
+ }
53
+ ) {}
54
+
55
+ export class CapabilityDeniedEvent extends Schema.TaggedClass<CapabilityDeniedEvent>()(
56
+ "CapabilityDenied",
57
+ {
58
+ capability: Schema.String,
59
+ operation: Schema.String
60
+ }
61
+ ) {}
62
+
63
+ export class CapabilityUnenforceable extends Schema.TaggedClass<CapabilityUnenforceable>()(
64
+ "CapabilityUnenforceable",
65
+ {
66
+ detail: Schema.String
67
+ }
68
+ ) {}
69
+
70
+ export class Declassified extends Schema.TaggedClass<Declassified>()("Declassified", {
71
+ label: Schema.String
72
+ }) {}
73
+
74
+ export const FlowEvent = Schema.Union([
75
+ StageStarted,
76
+ StageCompleted,
77
+ StageFailed,
78
+ Aborted,
79
+ Info,
80
+ ToolUse,
81
+ AssistantMessage,
82
+ TokensUsed,
83
+ CapabilityUsedEvent,
84
+ CapabilityDeniedEvent,
85
+ CapabilityUnenforceable,
86
+ Declassified
87
+ ])
88
+ export type FlowEvent = typeof FlowEvent.Type
89
+
90
+ export const FlowEventsValues = Object.freeze({
91
+ CapabilityUsed: (capability: string, operation: string): CapabilityUsedEvent =>
92
+ new CapabilityUsedEvent({ capability, operation }),
93
+ CapabilityDenied: (capability: string, operation: string): CapabilityDeniedEvent =>
94
+ new CapabilityDeniedEvent({ capability, operation }),
95
+ CapabilityUnenforceable: (detail: string): CapabilityUnenforceable =>
96
+ new CapabilityUnenforceable({ detail }),
97
+ Declassified: (label: string): Declassified => new Declassified({ label })
98
+ })
99
+
100
+ export interface FlowEventsShape {
101
+ readonly publish: (event: FlowEvent) => Effect.Effect<void>
102
+ }
103
+
104
+ export class FlowEvents extends Context.Service<FlowEvents, FlowEventsShape>()(
105
+ "@llm4ts/flow/FlowEvents"
106
+ ) {}
107
+
108
+ const noopFlowEvents = FlowEvents.of({
109
+ publish: (_event) => Effect.void
110
+ })
111
+
112
+ export const FlowEventsNoop = Layer.succeed(FlowEvents)(noopFlowEvents)
113
+
114
+ export interface CollectingFlowEvents extends FlowEventsShape {
115
+ readonly recorded: Effect.Effect<ReadonlyArray<FlowEvent>>
116
+ }
117
+
118
+ export const makeCollectingFlowEvents: Effect.Effect<CollectingFlowEvents> = Effect.map(
119
+ Ref.make<ReadonlyArray<FlowEvent>>([]),
120
+ (events) => ({
121
+ publish: (event) => Ref.update(events, (recorded) => [...recorded, event]),
122
+ recorded: Ref.get(events)
123
+ })
124
+ )
125
+
126
+ export interface FlowEventHub extends FlowEventsShape {
127
+ readonly subscribe: Effect.Effect<PubSub.Subscription<FlowEvent>, never, Scope.Scope>
128
+ readonly stream: Stream.Stream<FlowEvent>
129
+ readonly publishedCount: Effect.Effect<number>
130
+ }
131
+
132
+ export const makeFlowEventHub = Effect.fn("@llm4ts/flow/FlowEvents.makeHub")(function* (
133
+ capacity = 64
134
+ ): Effect.fn.Return<FlowEventHub> {
135
+ const events = yield* PubSub.bounded<FlowEvent>(capacity)
136
+ const published = yield* Ref.make(0)
137
+ return {
138
+ publish: (event) =>
139
+ Ref.update(published, (count) => count + 1).pipe(
140
+ Effect.andThen(PubSub.publish(events, event)),
141
+ Effect.asVoid
142
+ ),
143
+ subscribe: PubSub.subscribe(events),
144
+ stream: Stream.fromPubSub(events),
145
+ publishedCount: Ref.get(published)
146
+ }
147
+ })
@@ -0,0 +1,116 @@
1
+ import * as Clock from "effect/Clock"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Ref from "effect/Ref"
4
+ import * as Schema from "effect/Schema"
5
+ import * as Semaphore from "effect/Semaphore"
6
+ import type * as Scope from "effect/Scope"
7
+ import * as Stream from "effect/Stream"
8
+ import type { StreamRecorder } from "@llm4ts/core/observability/StreamRecorder"
9
+ import type { JsonValue } from "@llm4ts/core/providers/CliSupport"
10
+ import type { FlowEvent, FlowEventHub } from "./FlowEvents.ts"
11
+ import type { PlainFileStoreShape } from "./Persistence.ts"
12
+
13
+ export class TraceLine extends Schema.Class<TraceLine>("TraceLine")({
14
+ schemaVersion: Schema.optionalKey(Schema.Int),
15
+ seq: Schema.Int,
16
+ timestamp: Schema.Number,
17
+ runId: Schema.String,
18
+ kind: Schema.String,
19
+ fields: Schema.Record(Schema.String, Schema.Json)
20
+ }) {}
21
+
22
+ export interface FlowRecorderShape extends StreamRecorder {
23
+ readonly runId: string
24
+ readonly tracePath: string
25
+ readonly record: (event: FlowEvent) => Effect.Effect<void>
26
+ readonly consume: (hub: FlowEventHub) => Effect.Effect<void, never, Scope.Scope>
27
+ readonly awaitDrained: (hub: FlowEventHub) => Effect.Effect<void>
28
+ }
29
+
30
+ const encode = (line: TraceLine): string =>
31
+ JSON.stringify({
32
+ schemaVersion: line.schemaVersion,
33
+ seq: line.seq,
34
+ timestamp: line.timestamp,
35
+ runId: line.runId,
36
+ kind: line.kind,
37
+ fields: line.fields
38
+ })
39
+
40
+ export const makeFlowRecorder = Effect.fn("@llm4ts/flow/FlowRecorder.make")(function* (
41
+ files: PlainFileStoreShape,
42
+ tracePath: string,
43
+ runId: string
44
+ ): Effect.fn.Return<FlowRecorderShape> {
45
+ const sequence = yield* Ref.make(0)
46
+ const consumed = yield* Ref.make(0)
47
+ const lock = yield* Semaphore.make(1)
48
+ const degraded = yield* Ref.make(false)
49
+
50
+ const append = (kind: string, fields: Readonly<Record<string, JsonValue>>): Effect.Effect<void> =>
51
+ lock.withPermit(
52
+ Effect.gen(function* () {
53
+ if (yield* Ref.get(degraded)) {
54
+ return
55
+ }
56
+ const seq = yield* Ref.getAndUpdate(sequence, (current) => current + 1)
57
+ const timestamp = yield* Clock.currentTimeMillis
58
+ const line = TraceLine.make({
59
+ schemaVersion: 1,
60
+ seq,
61
+ timestamp,
62
+ runId,
63
+ kind,
64
+ fields
65
+ })
66
+ yield* files
67
+ .append(tracePath, `${encode(line)}\n`)
68
+ .pipe(Effect.catch(() => Ref.set(degraded, true)))
69
+ })
70
+ )
71
+
72
+ const record = (event: FlowEvent): Effect.Effect<void> =>
73
+ append(event._tag, { event: JSON.stringify(event) })
74
+
75
+ const recorder: FlowRecorderShape = {
76
+ runId,
77
+ tracePath,
78
+ record,
79
+ rawLine: (provider, model, line) =>
80
+ append("RawLine", {
81
+ provider,
82
+ ...(model === undefined ? {} : { model }),
83
+ line
84
+ }),
85
+ streamError: (provider, model, message) =>
86
+ append("StreamError", {
87
+ provider,
88
+ ...(model === undefined ? {} : { model }),
89
+ message
90
+ }),
91
+ consume: (hub) =>
92
+ Effect.gen(function* () {
93
+ const subscription = yield* hub.subscribe
94
+ yield* Stream.fromSubscription(subscription).pipe(
95
+ Stream.runForEach((event) =>
96
+ record(event).pipe(Effect.andThen(Ref.update(consumed, (count) => count + 1)))
97
+ ),
98
+ Effect.forkScoped,
99
+ Effect.asVoid
100
+ )
101
+ }),
102
+ awaitDrained: (hub) =>
103
+ Effect.gen(function* () {
104
+ const target = yield* hub.publishedCount
105
+ const drain: Effect.Effect<void> = Effect.suspend(() =>
106
+ Ref.get(consumed).pipe(
107
+ Effect.flatMap((count) =>
108
+ count >= target ? Effect.void : Effect.yieldNow.pipe(Effect.andThen(drain))
109
+ )
110
+ )
111
+ )
112
+ yield* drain
113
+ })
114
+ }
115
+ return recorder
116
+ })
@@ -0,0 +1,315 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Schema from "effect/Schema"
3
+ import { Capabilities } from "@llm4ts/core/Capability"
4
+ import type { ProcessExecutorShape, ProcessResult } from "@llm4ts/core/ProcessExecutor"
5
+ import { ProcessError, type FlowError } from "./FlowError.ts"
6
+ import type { FlowEventsShape } from "./FlowEvents.ts"
7
+ import { guarded } from "./CapabilityGuard.ts"
8
+
9
+ export class IssueRef extends Schema.Class<IssueRef>("IssueRef")({
10
+ owner: Schema.String,
11
+ repo: Schema.String,
12
+ number: Schema.Int
13
+ }) {
14
+ get shortRef(): string {
15
+ return `${this.owner}/${this.repo}#${this.number}`
16
+ }
17
+ }
18
+
19
+ export const parseIssueRef = (input: string): IssueRef | undefined => {
20
+ const match = /^([^/\s]+)\/([^#\s]+)#(\d+)$/.exec(input.trim())
21
+ const owner = match?.[1]
22
+ const repo = match?.[2]
23
+ const number = Number.parseInt(match?.[3] ?? "", 10)
24
+ return owner === undefined || repo === undefined || !Number.isInteger(number) || number <= 0
25
+ ? undefined
26
+ : IssueRef.make({ owner, repo, number })
27
+ }
28
+
29
+ export class Issue extends Schema.Class<Issue>("Issue")({
30
+ title: Schema.String,
31
+ body: Schema.String,
32
+ author: Schema.String
33
+ }) {}
34
+
35
+ export class PullRequest extends Schema.Class<PullRequest>("PullRequest")({
36
+ owner: Schema.String,
37
+ repo: Schema.String,
38
+ number: Schema.Int,
39
+ url: Schema.String
40
+ }) {
41
+ get shortRef(): string {
42
+ return `${this.owner}/${this.repo}#${this.number}`
43
+ }
44
+ }
45
+
46
+ export const BuildOutcome = Schema.Literals(["Success", "Failure", "Pending", "TimedOut"])
47
+ export type BuildOutcome = typeof BuildOutcome.Type
48
+
49
+ export const parsePullRequestUrl = (url: string): PullRequest | undefined => {
50
+ const match = /^https?:\/\/[^/]+\/([^/]+)\/([^/]+)\/pull\/(\d+).*$/.exec(url.trim())
51
+ const owner = match?.[1]
52
+ const repo = match?.[2]
53
+ const number = Number.parseInt(match?.[3] ?? "", 10)
54
+ return owner === undefined || repo === undefined || !Number.isInteger(number)
55
+ ? undefined
56
+ : PullRequest.make({
57
+ owner,
58
+ repo,
59
+ number,
60
+ url: url.trim()
61
+ })
62
+ }
63
+
64
+ export const prViewArgs = ["pr", "view", "--json", "url", "--jq", ".url"]
65
+
66
+ export const prCreateArgs = (
67
+ title: string,
68
+ body: string,
69
+ base: string | undefined,
70
+ draft: boolean
71
+ ): ReadonlyArray<string> => [
72
+ "pr",
73
+ "create",
74
+ "--title",
75
+ title,
76
+ "--body",
77
+ body,
78
+ ...(base === undefined ? [] : ["--base", base]),
79
+ ...(draft ? ["--draft"] : [])
80
+ ]
81
+
82
+ export const issueViewArgs = (ref: IssueRef): ReadonlyArray<string> => [
83
+ "issue",
84
+ "view",
85
+ String(ref.number),
86
+ "--repo",
87
+ `${ref.owner}/${ref.repo}`,
88
+ "--json",
89
+ "title,body,author"
90
+ ]
91
+
92
+ export const issueCommentArgs = (ref: IssueRef, body: string): ReadonlyArray<string> => [
93
+ "issue",
94
+ "comment",
95
+ String(ref.number),
96
+ "--repo",
97
+ `${ref.owner}/${ref.repo}`,
98
+ "--body",
99
+ body
100
+ ]
101
+
102
+ export const prCommentArgs = (pr: PullRequest, body: string): ReadonlyArray<string> => [
103
+ "pr",
104
+ "comment",
105
+ String(pr.number),
106
+ "--repo",
107
+ `${pr.owner}/${pr.repo}`,
108
+ "--body",
109
+ body
110
+ ]
111
+
112
+ export const prPatchArgs = (
113
+ pr: PullRequest,
114
+ title: string,
115
+ body: string
116
+ ): ReadonlyArray<string> => [
117
+ "api",
118
+ "--method",
119
+ "PATCH",
120
+ `repos/${pr.owner}/${pr.repo}/pulls/${pr.number}`,
121
+ "-f",
122
+ `title=${title}`,
123
+ "-f",
124
+ `body=${body}`
125
+ ]
126
+
127
+ export const prChecksArgs = (pr: PullRequest): ReadonlyArray<string> => [
128
+ "pr",
129
+ "view",
130
+ String(pr.number),
131
+ "--repo",
132
+ `${pr.owner}/${pr.repo}`,
133
+ "--json",
134
+ "statusCheckRollup"
135
+ ]
136
+
137
+ class GhAuthor extends Schema.Class<GhAuthor>("GhAuthor")({
138
+ login: Schema.String
139
+ }) {}
140
+
141
+ class GhIssue extends Schema.Class<GhIssue>("GhIssue")({
142
+ title: Schema.String,
143
+ body: Schema.String,
144
+ author: GhAuthor
145
+ }) {}
146
+
147
+ class GhCheck extends Schema.Class<GhCheck>("GhCheck")({
148
+ status: Schema.optionalKey(Schema.String),
149
+ conclusion: Schema.optionalKey(Schema.String),
150
+ state: Schema.optionalKey(Schema.String)
151
+ }) {}
152
+
153
+ class GhChecks extends Schema.Class<GhChecks>("GhChecks")({
154
+ statusCheckRollup: Schema.Array(GhCheck).pipe(
155
+ Schema.withConstructorDefault(Effect.succeed(Object.freeze([])))
156
+ )
157
+ }) {}
158
+
159
+ export const parseIssue = (json: string): Effect.Effect<Issue, ProcessError> =>
160
+ Schema.decodeUnknownEffect(Schema.fromJsonString(GhIssue))(json).pipe(
161
+ Effect.map(
162
+ (issue) =>
163
+ new Issue({
164
+ title: issue.title,
165
+ body: issue.body,
166
+ author: issue.author.login
167
+ })
168
+ ),
169
+ Effect.mapError((error) =>
170
+ ProcessError.make({
171
+ message: "gh issue view",
172
+ detail: `invalid issue JSON: ${String(error)}`
173
+ })
174
+ )
175
+ )
176
+
177
+ export const outcomeFromChecksJson = (json: string): Effect.Effect<BuildOutcome, ProcessError> =>
178
+ Schema.decodeUnknownEffect(Schema.fromJsonString(GhChecks))(json).pipe(
179
+ Effect.map((parsed) => {
180
+ const pending = parsed.statusCheckRollup.some(
181
+ (check) =>
182
+ (check.status !== undefined && check.status.toUpperCase() !== "COMPLETED") ||
183
+ ["PENDING", "EXPECTED"].includes(check.state?.toUpperCase() ?? "")
184
+ )
185
+ const failed = parsed.statusCheckRollup.some(
186
+ (check) =>
187
+ ["FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STARTUP_FAILURE"].includes(
188
+ check.conclusion?.toUpperCase() ?? ""
189
+ ) || ["FAILURE", "ERROR"].includes(check.state?.toUpperCase() ?? "")
190
+ )
191
+ return pending ? "Pending" : failed ? "Failure" : "Success"
192
+ }),
193
+ Effect.mapError((error) =>
194
+ ProcessError.make({
195
+ message: "gh pr checks",
196
+ detail: `invalid checks JSON: ${String(error)}`
197
+ })
198
+ )
199
+ )
200
+
201
+ export interface GitHubToolShape {
202
+ readonly createPr: (
203
+ title: string,
204
+ body: string,
205
+ base?: string,
206
+ draft?: boolean
207
+ ) => Effect.Effect<PullRequest, FlowError>
208
+ readonly readIssue: (ref: IssueRef) => Effect.Effect<Issue, FlowError>
209
+ readonly writeIssueComment: (ref: IssueRef, body: string) => Effect.Effect<void, FlowError>
210
+ readonly writePrComment: (pr: PullRequest, body: string) => Effect.Effect<void, FlowError>
211
+ readonly updatePr: (
212
+ pr: PullRequest,
213
+ title: string,
214
+ body: string
215
+ ) => Effect.Effect<void, FlowError>
216
+ readonly prChecks: (pr: PullRequest) => Effect.Effect<BuildOutcome, FlowError>
217
+ }
218
+
219
+ const output = (result: ProcessResult): string => result.stdout.join("\n").trim()
220
+
221
+ export const makeGitHubTool = (
222
+ process: ProcessExecutorShape,
223
+ workDir: string,
224
+ events: FlowEventsShape
225
+ ): GitHubToolShape => {
226
+ const run = (
227
+ args: ReadonlyArray<string>,
228
+ allowFailure = false
229
+ ): Effect.Effect<ProcessResult, FlowError> =>
230
+ process.run(["gh", ...args], workDir, {}).pipe(
231
+ Effect.mapError((error) =>
232
+ ProcessError.make({
233
+ message: `gh ${args.join(" ")}`,
234
+ detail: error.message
235
+ })
236
+ ),
237
+ Effect.flatMap((result) =>
238
+ result.exitCode === 0 || allowFailure
239
+ ? Effect.succeed(result)
240
+ : Effect.fail(
241
+ ProcessError.make({
242
+ message: `gh ${args.join(" ")}`,
243
+ detail:
244
+ [...result.stdout, ...result.stderr].join("\n").trim() ||
245
+ `exit code ${result.exitCode}`
246
+ })
247
+ )
248
+ )
249
+ )
250
+
251
+ const read = <A>(
252
+ operation: string,
253
+ effect: Effect.Effect<A, FlowError>
254
+ ): Effect.Effect<A, FlowError> => guarded(Capabilities.GhRead, operation, events, effect)
255
+ const write = <A>(
256
+ operation: string,
257
+ effect: Effect.Effect<A, FlowError>
258
+ ): Effect.Effect<A, FlowError> => guarded(Capabilities.GhWrite, operation, events, effect)
259
+
260
+ const findOpenPr = run(prViewArgs, true).pipe(
261
+ Effect.map((result) =>
262
+ result.exitCode === 0
263
+ ? output(result)
264
+ .split(/\r?\n/)
265
+ .map(parsePullRequestUrl)
266
+ .find((item) => item !== undefined)
267
+ : undefined
268
+ )
269
+ )
270
+
271
+ return {
272
+ createPr: (title, body, base, draft = false) =>
273
+ write(
274
+ "gh pr create",
275
+ Effect.flatMap(findOpenPr, (existing) =>
276
+ existing === undefined
277
+ ? run(prCreateArgs(title, body, base, draft)).pipe(
278
+ Effect.flatMap((result) => {
279
+ const pr = output(result)
280
+ .split(/\r?\n/)
281
+ .map(parsePullRequestUrl)
282
+ .find((item) => item !== undefined)
283
+ return pr === undefined
284
+ ? Effect.fail(
285
+ ProcessError.make({
286
+ message: "gh pr create",
287
+ detail: `could not parse a PR URL from: ${output(result)}`
288
+ })
289
+ )
290
+ : Effect.succeed(pr)
291
+ })
292
+ )
293
+ : Effect.succeed(existing)
294
+ )
295
+ ),
296
+ readIssue: (ref) =>
297
+ read(
298
+ "gh issue view",
299
+ run(issueViewArgs(ref)).pipe(Effect.flatMap((result) => parseIssue(output(result))))
300
+ ),
301
+ writeIssueComment: (ref, body) =>
302
+ write("gh issue comment", run(issueCommentArgs(ref, body)).pipe(Effect.asVoid)),
303
+ writePrComment: (pr, body) =>
304
+ write("gh pr comment", run(prCommentArgs(pr, body)).pipe(Effect.asVoid)),
305
+ updatePr: (pr, title, body) =>
306
+ write("gh pr edit", run(prPatchArgs(pr, title, body)).pipe(Effect.asVoid)),
307
+ prChecks: (pr) =>
308
+ read(
309
+ "gh pr checks",
310
+ run(prChecksArgs(pr)).pipe(
311
+ Effect.flatMap((result) => outcomeFromChecksJson(output(result)))
312
+ )
313
+ )
314
+ }
315
+ }