@llm4ts/runner 3.4.0 → 4.0.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 (84) hide show
  1. package/dist/Connectors.d.ts +0 -6
  2. package/dist/Connectors.d.ts.map +1 -1
  3. package/dist/Connectors.js +0 -9
  4. package/dist/Connectors.js.map +1 -1
  5. package/dist/EventPresentation.d.ts.map +1 -1
  6. package/dist/EventPresentation.js +0 -1
  7. package/dist/EventPresentation.js.map +1 -1
  8. package/dist/FlowRunner.d.ts +10 -3
  9. package/dist/FlowRunner.d.ts.map +1 -1
  10. package/dist/FlowRunner.js +20 -2
  11. package/dist/FlowRunner.js.map +1 -1
  12. package/dist/NodeGeminiCliExecutor.d.ts.map +1 -1
  13. package/dist/NodeGeminiCliExecutor.js +1 -0
  14. package/dist/NodeGeminiCliExecutor.js.map +1 -1
  15. package/dist/NodeGeminiModelBridge.d.ts.map +1 -1
  16. package/dist/NodeGeminiModelBridge.js +1 -0
  17. package/dist/NodeGeminiModelBridge.js.map +1 -1
  18. package/dist/NodeHttpClient.d.ts.map +1 -1
  19. package/dist/NodeHttpClient.js +1 -0
  20. package/dist/NodeHttpClient.js.map +1 -1
  21. package/dist/Otel.d.ts.map +1 -1
  22. package/dist/Otel.js +1 -0
  23. package/dist/Otel.js.map +1 -1
  24. package/dist/Package.d.ts.map +1 -1
  25. package/dist/Package.js +1 -0
  26. package/dist/Package.js.map +1 -1
  27. package/dist/Terminal.d.ts +1 -2
  28. package/dist/Terminal.d.ts.map +1 -1
  29. package/dist/Terminal.js +1 -4
  30. package/dist/Terminal.js.map +1 -1
  31. package/dist/WindowsCommand.d.ts.map +1 -1
  32. package/dist/WindowsCommand.js +1 -0
  33. package/dist/WindowsCommand.js.map +1 -1
  34. package/dist/index.d.ts +1 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -2
  37. package/dist/index.js.map +1 -1
  38. package/package.json +8 -21
  39. package/src/Connectors.ts +0 -10
  40. package/src/EventPresentation.ts +0 -1
  41. package/src/FlowRunner.ts +26 -6
  42. package/src/NodeGeminiCliExecutor.ts +1 -0
  43. package/src/NodeGeminiModelBridge.ts +1 -0
  44. package/src/NodeHttpClient.ts +1 -0
  45. package/src/Otel.ts +1 -0
  46. package/src/Package.ts +1 -0
  47. package/src/Terminal.ts +1 -5
  48. package/src/WindowsCommand.ts +1 -0
  49. package/src/index.ts +0 -2
  50. package/dist/Costs.d.ts +0 -52
  51. package/dist/Costs.d.ts.map +0 -1
  52. package/dist/Costs.js +0 -95
  53. package/dist/Costs.js.map +0 -1
  54. package/dist/McpStdio.d.ts +0 -7
  55. package/dist/McpStdio.d.ts.map +0 -1
  56. package/dist/McpStdio.js +0 -19
  57. package/dist/McpStdio.js.map +0 -1
  58. package/dist/Profile.d.ts +0 -313
  59. package/dist/Profile.d.ts.map +0 -1
  60. package/dist/Profile.js +0 -937
  61. package/dist/Profile.js.map +0 -1
  62. package/dist/ReviewFingerprint.d.ts +0 -2
  63. package/dist/ReviewFingerprint.d.ts.map +0 -1
  64. package/dist/ReviewFingerprint.js +0 -13
  65. package/dist/ReviewFingerprint.js.map +0 -1
  66. package/dist/RunReaders.d.ts +0 -70
  67. package/dist/RunReaders.d.ts.map +0 -1
  68. package/dist/RunReaders.js +0 -236
  69. package/dist/RunReaders.js.map +0 -1
  70. package/dist/StoryReaders.d.ts +0 -16
  71. package/dist/StoryReaders.d.ts.map +0 -1
  72. package/dist/StoryReaders.js +0 -107
  73. package/dist/StoryReaders.js.map +0 -1
  74. package/dist/Watch.d.ts +0 -68
  75. package/dist/Watch.d.ts.map +0 -1
  76. package/dist/Watch.js +0 -261
  77. package/dist/Watch.js.map +0 -1
  78. package/src/Costs.ts +0 -148
  79. package/src/McpStdio.ts +0 -41
  80. package/src/Profile.ts +0 -1196
  81. package/src/ReviewFingerprint.ts +0 -13
  82. package/src/RunReaders.ts +0 -370
  83. package/src/StoryReaders.ts +0 -157
  84. package/src/Watch.ts +0 -446
@@ -1,13 +0,0 @@
1
- import { createHash } from "node:crypto"
2
-
3
- export const reviewFingerprint = (...parts: ReadonlyArray<string>): string => {
4
- const digest = createHash("sha256")
5
- for (const part of parts) {
6
- const bytes = Buffer.from(part, "utf8")
7
- const length = Buffer.alloc(4)
8
- length.writeUInt32BE(bytes.length)
9
- digest.update(length)
10
- digest.update(bytes)
11
- }
12
- return digest.digest("hex")
13
- }
package/src/RunReaders.ts DELETED
@@ -1,370 +0,0 @@
1
- import { join } from "node:path"
2
- import * as Effect from "effect/Effect"
3
- import * as Option from "effect/Option"
4
- import * as Schema from "effect/Schema"
5
- import { Board, BoardVersion } from "@llm4ts/flow/BoardSync"
6
- import { loadCostLedger } from "@llm4ts/flow/CostLedger"
7
- import {
8
- buildCostReport,
9
- reconcileWithLedger,
10
- usageSamplesFromTrace
11
- } from "@llm4ts/flow/CostReport"
12
- import { describeFlowError, type FlowError } from "@llm4ts/flow/FlowError"
13
- import { FlowEvent } from "@llm4ts/flow/FlowEvents"
14
- import { RunOutcome, type TraceLine } from "@llm4ts/flow/FlowRecorder"
15
- import {
16
- CostsOut,
17
- Diagnosis,
18
- RunStatus,
19
- type LaneStatusOut,
20
- type MeaningfulEvent,
21
- type OperatorRunState
22
- } from "@llm4ts/flow/OperatorSchemas"
23
- import { loadVersioned, type PlainFileStoreShape } from "@llm4ts/flow/Persistence"
24
- import { parsePlan } from "@llm4ts/flow/Plan"
25
- import { readTrace } from "@llm4ts/flow/Replay"
26
- import { emptyTree, reduceTree, treeInputsOfTrace, type TreeState } from "./AgentTree.ts"
27
- import { digestedTags, presentationOf, type DigestLine } from "./EventPresentation.ts"
28
- import { summarizeTree } from "./RunSummary.ts"
29
- import { loadTranscript, tailLinesOf, type TranscriptFiles } from "./Transcripts.ts"
30
-
31
- /**
32
- * Read-only answers about a run, from the files it writes: how it stands,
33
- * what happened since a sequence number, why it failed, what it cost. Every
34
- * reader takes the file store as a parameter, so a test feeds it a memory
35
- * store and the MCP operator feeds it the real one.
36
- */
37
-
38
- export class TraceUnreadable extends Schema.TaggedError<TraceUnreadable>()("TraceUnreadable", {
39
- path: Schema.String,
40
- message: Schema.String
41
- }) {}
42
-
43
- export interface RunSources {
44
- readonly files: PlainFileStoreShape
45
- /**
46
- * Relative paths of every regular file under a directory, recursively (e.g.
47
- * "task-2/1-pnpm-test.log"), never directory names; none when it does not exist.
48
- */
49
- readonly listFiles: (directory: string) => ReadonlyArray<string>
50
- }
51
-
52
- export interface TraceFold {
53
- readonly lines: ReadonlyArray<TraceLine>
54
- readonly state: TreeState
55
- readonly seq: number
56
- }
57
-
58
- const decodeEvent = Schema.decodeUnknownOption(Schema.fromJsonString(FlowEvent))
59
- const isOutcome = Schema.is(RunOutcome)
60
-
61
- const eventOf = (line: TraceLine): FlowEvent | undefined => {
62
- const raw = line.fields.event
63
- return typeof raw === "string" ? Option.getOrUndefined(decodeEvent(raw)) : undefined
64
- }
65
-
66
- const unreadable =
67
- (path: string) =>
68
- (error: FlowError): TraceUnreadable =>
69
- TraceUnreadable.make({ path, message: describeFlowError(error) })
70
-
71
- const readLines = (
72
- files: PlainFileStoreShape,
73
- path: string
74
- ): Effect.Effect<ReadonlyArray<TraceLine>, TraceUnreadable> =>
75
- readTrace(files, path).pipe(Effect.mapError(unreadable(path)))
76
-
77
- const lastSeq = (lines: ReadonlyArray<TraceLine>): number => lines[lines.length - 1]?.seq ?? 0
78
-
79
- /** The trace folded into the watch TUI's tree, seeded from the epic's board when there is one. */
80
- export const foldTrace = (
81
- files: PlainFileStoreShape,
82
- tracePath: string,
83
- boardDir?: string
84
- ): Effect.Effect<TraceFold, TraceUnreadable> =>
85
- Effect.gen(function* () {
86
- const lines = yield* readLines(files, tracePath)
87
- const board =
88
- boardDir === undefined
89
- ? undefined
90
- : yield* loadVersioned(files, join(boardDir, "board.json"), BoardVersion, Board).pipe(
91
- Effect.catch(() => Effect.succeed(undefined))
92
- )
93
- const start = emptyTree(
94
- board === undefined
95
- ? {}
96
- : {
97
- title: board.title.replace(/^Epic:\s*/u, "epic "),
98
- stories: board.items.map((item) => ({ id: item.id, status: item.status }))
99
- }
100
- )
101
- const state = treeInputsOfTrace(lines).reduce(reduceTree, start)
102
- return { lines, state, seq: lastSeq(lines) }
103
- })
104
-
105
- export const runStatusOf = (
106
- fold: TraceFold,
107
- run: {
108
- readonly runId: string
109
- readonly flow: string
110
- readonly state: OperatorRunState
111
- },
112
- now: number
113
- ): RunStatus => {
114
- const { state } = fold
115
- const lanes = state.lanes.map((lane): LaneStatusOut => {
116
- const stage = lane.stages[lane.stages.length - 1]
117
- return {
118
- id: lane.id,
119
- status: lane.status,
120
- ...(stage === undefined ? {} : { stage }),
121
- ...(lane.executor === undefined ? {} : { executor: lane.executor }),
122
- ...(lane.activity === undefined ? {} : { activity: lane.activity.text }),
123
- lastEventAgeMs: Math.max(0, Math.round(now - lane.lastEventAt)),
124
- costUsd: lane.costUsd
125
- }
126
- })
127
- const unenforceable = fold.lines.flatMap((line) => {
128
- const event = eventOf(line)
129
- return event?._tag === "CapabilityUnenforceable" ? [event.detail] : []
130
- })
131
- const end = state.ended === undefined ? now : (state.now ?? now)
132
- return RunStatus.make({
133
- runId: run.runId,
134
- state: run.state,
135
- ...(state.ended !== undefined && isOutcome(state.ended) ? { ended: state.ended } : {}),
136
- elapsedMs: state.startedAt === undefined ? 0 : Math.max(0, Math.round(end - state.startedAt)),
137
- lanes,
138
- stories: state.stories.map((story) => ({ id: story.id, status: story.status })),
139
- tokens: Math.round(state.tokens),
140
- costUsd: state.costUsd,
141
- warnings: [
142
- ...unenforceable,
143
- ...state.exclusions.map((entry) => `${entry.executor} excluded: ${entry.reason}`)
144
- ],
145
- seq: fold.seq,
146
- summary: summarizeTree(state, run.flow)
147
- })
148
- }
149
-
150
- /** Trace kinds a digest shows: the events with a digest line, and the run's end (a trace line, not an event). */
151
- export const meaningfulKinds: ReadonlySet<string> = new Set([...digestedTags, "RunEnded"])
152
-
153
- const textOf = (event: FlowEvent): DigestLine | undefined => presentationOf(event).digest?.(event)
154
-
155
- export const meaningfulEventsAfter = (
156
- lines: ReadonlyArray<TraceLine>,
157
- sinceSeq: number
158
- ): ReadonlyArray<MeaningfulEvent> =>
159
- lines.flatMap((line): ReadonlyArray<MeaningfulEvent> => {
160
- if (line.seq <= sinceSeq || !meaningfulKinds.has(line.kind)) {
161
- return []
162
- }
163
- if (line.kind === "RunEnded") {
164
- const outcome = line.fields.outcome
165
- return [
166
- {
167
- seq: line.seq,
168
- at: line.timestamp,
169
- kind: line.kind,
170
- text: `run ${typeof outcome === "string" ? outcome : "completed"}`
171
- }
172
- ]
173
- }
174
- const event = eventOf(line)
175
- const rendered = event === undefined ? undefined : textOf(event)
176
- return rendered === undefined
177
- ? []
178
- : [
179
- {
180
- seq: line.seq,
181
- at: line.timestamp,
182
- kind: line.kind,
183
- ...(rendered.lane === undefined ? {} : { lane: rendered.lane }),
184
- text: rendered.text
185
- }
186
- ]
187
- })
188
-
189
- export const eventsAfter = (
190
- lines: ReadonlyArray<TraceLine>,
191
- sinceSeq: number,
192
- kinds: ReadonlyArray<string> | undefined,
193
- limit: number
194
- ): ReadonlyArray<TraceLine> =>
195
- lines
196
- .filter(
197
- (line) =>
198
- line.seq > sinceSeq && (kinds === undefined || kinds.some((kind) => kind === line.kind))
199
- )
200
- .slice(0, Math.max(0, limit))
201
-
202
- const digestTail = 40
203
- const tailRows = 30
204
- const tailWidth = 120
205
-
206
- const quiet = <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A | undefined> =>
207
- effect.pipe(Effect.catch(() => Effect.succeed(undefined)))
208
-
209
- const capText = (text: string, remaining: number): string => text.slice(0, Math.max(0, remaining))
210
-
211
- /** The last rows of each log in a directory, within what is left of a character budget. */
212
- export const tailGateLogs = (
213
- sources: RunSources,
214
- directory: string | undefined,
215
- budget: number
216
- ): Effect.Effect<{
217
- readonly tails: ReadonlyArray<{ readonly path: string; readonly tail: string }>
218
- readonly remaining: number
219
- }> =>
220
- Effect.gen(function* () {
221
- const tails: Array<{ path: string; tail: string }> = []
222
- let remaining = budget
223
- if (directory !== undefined) {
224
- for (const name of sources.listFiles(directory)) {
225
- const path = join(directory, name)
226
- const text = yield* quiet(sources.files.read(path))
227
- if (text === undefined || remaining <= 0) continue
228
- const tail = capText(text.trimEnd().split(/\r?\n/u).slice(-tailRows).join("\n"), remaining)
229
- remaining -= tail.length
230
- tails.push({ path, tail })
231
- }
232
- }
233
- return { tails, remaining }
234
- })
235
-
236
- /** The last rows of one lane's transcript, capped to what is left of a character budget. */
237
- export const tailTranscript = (
238
- sources: RunSources,
239
- directory: string | undefined,
240
- lane: string,
241
- budget: number
242
- ): Effect.Effect<ReadonlyArray<string>> =>
243
- Effect.gen(function* () {
244
- const out: Array<string> = []
245
- if (directory === undefined) return out
246
- let remaining = budget
247
- const transcriptFiles: TranscriptFiles = {
248
- read: (path) => quiet(sources.files.read(path)),
249
- list: sources.listFiles
250
- }
251
- const entries = yield* loadTranscript(transcriptFiles, directory, { lane })
252
- const rows = tailLinesOf(entries, { lane }, undefined, tailWidth, tailRows) ?? []
253
- for (const row of rows) {
254
- if (remaining <= 0) break
255
- const kept = capText(row, remaining)
256
- remaining -= kept.length
257
- out.push(kept)
258
- }
259
- return out
260
- })
261
-
262
- export const runDiagnosisOf = (
263
- sources: RunSources,
264
- args: {
265
- readonly runId: string
266
- readonly tracePath: string
267
- readonly planPath: string | undefined
268
- readonly transcriptsDir: string | undefined
269
- readonly gateLogDir: string | undefined
270
- readonly budget: number
271
- }
272
- ): Effect.Effect<Diagnosis, TraceUnreadable> =>
273
- Effect.gen(function* () {
274
- const lines = yield* readLines(sources.files, args.tracePath)
275
- const events = lines.flatMap((line) => {
276
- const event = eventOf(line)
277
- return event === undefined ? [] : [event]
278
- })
279
- const failed = events.filter(
280
- (event) => event._tag === "StageFailed" || event._tag === "Aborted"
281
- )
282
- const last = failed[failed.length - 1]
283
- const failure =
284
- last === undefined
285
- ? undefined
286
- : last._tag === "StageFailed"
287
- ? {
288
- stage: last.stage,
289
- message: last.message,
290
- ...(last.lane === undefined ? {} : { lane: last.lane })
291
- }
292
- : last._tag === "Aborted"
293
- ? { message: last.message }
294
- : undefined
295
-
296
- const markdown =
297
- args.planPath === undefined ? undefined : yield* quiet(sources.files.read(args.planPath))
298
- const plan = markdown === undefined ? undefined : yield* quiet(parsePlan(markdown))
299
- const taskLines = (plan?.tasks ?? []).map(
300
- (task) => `- [${task.completed ? "x" : " "}] ${task.title}`
301
- )
302
- const recent = events
303
- .flatMap((event) =>
304
- event._tag === "Info"
305
- ? [event.message]
306
- : event._tag === "StageFailed"
307
- ? [`${event.stage} failed: ${event.message}`]
308
- : []
309
- )
310
- .slice(-digestTail)
311
- const digest = capText([...taskLines, ...recent].join("\n"), args.budget)
312
- const remaining = args.budget - digest.length
313
-
314
- const gateTails = yield* tailGateLogs(sources, args.gateLogDir, remaining)
315
- const transcript = yield* tailTranscript(
316
- sources,
317
- args.transcriptsDir,
318
- "run",
319
- gateTails.remaining
320
- )
321
-
322
- return Diagnosis.make({
323
- runId: args.runId,
324
- digest,
325
- ...(failure === undefined ? {} : { failure }),
326
- gateLogTails: gateTails.tails,
327
- transcriptTail: transcript
328
- })
329
- })
330
-
331
- const ledgerTolerance = 0.005
332
-
333
- const costOf = (report: {
334
- readonly measured: { readonly costUsd?: number }
335
- readonly estimated: { readonly costUsd?: number }
336
- }): number => (report.measured.costUsd ?? 0) + (report.estimated.costUsd ?? 0)
337
-
338
- /**
339
- * One run's costs: its trace folded and topped up from its `costs.jsonl`
340
- * line (`CostReport.reconcileWithLedger`), so `total` is what the run itself
341
- * printed even when the trace lost lines; `fromLedger` is how much of it the
342
- * ledger supplied. `differs` is left for a trace that says more than the
343
- * ledger, which no line can fill.
344
- */
345
- export const costsOfRun = (
346
- files: PlainFileStoreShape,
347
- tracePath: string,
348
- ledgerPath: string,
349
- runId: string
350
- ): Effect.Effect<CostsOut, TraceUnreadable> =>
351
- Effect.gen(function* () {
352
- const lines = yield* readLines(files, tracePath)
353
- const traced = usageSamplesFromTrace(lines)
354
- const records = yield* loadCostLedger(files, ledgerPath).pipe(
355
- Effect.mapError(unreadable(ledgerPath))
356
- )
357
- const record = records.find((candidate) => candidate.runId === runId)
358
- const reconciled = record === undefined ? traced : reconcileWithLedger(traced, [record]).samples
359
- const report = buildCostReport(reconciled)
360
- const total = costOf(report)
361
- const fromLedger = total - costOf(buildCostReport(traced))
362
- const ledger = record?.totalCostUsd
363
- return CostsOut.make({
364
- report,
365
- total,
366
- ...(ledger === undefined ? {} : { ledger }),
367
- ...(fromLedger > ledgerTolerance ? { fromLedger } : {}),
368
- differs: ledger !== undefined && Math.abs(total - ledger) > ledgerTolerance
369
- })
370
- })
@@ -1,157 +0,0 @@
1
- import { join } from "node:path"
2
- import * as Effect from "effect/Effect"
3
- import * as Option from "effect/Option"
4
- import * as Schema from "effect/Schema"
5
- import { describeFlowError, type FlowError } from "@llm4ts/flow/FlowError"
6
- import { Diagnosis, StoryDetail, UnknownStory } from "@llm4ts/flow/OperatorSchemas"
7
- import {
8
- loadEpicFiles,
9
- loadStoryFiles,
10
- renderStoryDigest,
11
- type RetroInputs
12
- } from "@llm4ts/flow/Retro"
13
- import { readTrace } from "@llm4ts/flow/Replay"
14
- import { makeStoryPlanStore, type Story, type StoryPlan } from "@llm4ts/flow/StoryPlan"
15
- import { TraceUnreadable, tailGateLogs, tailTranscript, type RunSources } from "./RunReaders.ts"
16
- import { loadTranscript, type TranscriptFiles } from "./Transcripts.ts"
17
-
18
- /**
19
- * Read-only answers about one story of an epic run, from the files the story
20
- * executor keeps under `<stateDir>/stories/<id>.*` and `<stateDir>/board.json`.
21
- */
22
-
23
- const safeId = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u
24
-
25
- /** A story id is a file-name stem: no separators, no dots-only, nothing that climbs. */
26
- export const isSafeStoryId = (id: string): boolean => safeId.test(id)
27
-
28
- const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json))
29
- const encodeJson = (value: unknown): string | undefined => JSON.stringify(value)
30
-
31
- const planPathOf = (stateDir: string): string => join(stateDir, "plan.md")
32
-
33
- const unknownStory = (storyId: string): UnknownStory =>
34
- UnknownStory.make({ message: `unknown story '${storyId}'` })
35
-
36
- /** The plan's story, or `UnknownStory` for an unsafe id, a missing plan or an id the plan lacks. */
37
- const storyOf = (
38
- sources: RunSources,
39
- stateDir: string,
40
- storyId: string
41
- ): Effect.Effect<{ readonly plan: StoryPlan; readonly story: Story }, FlowError | UnknownStory> =>
42
- Effect.gen(function* () {
43
- if (!isSafeStoryId(storyId)) {
44
- return yield* unknownStory(storyId)
45
- }
46
- const plan = yield* makeStoryPlanStore(sources.files).load(planPathOf(stateDir))
47
- const story = plan?.story(storyId)
48
- if (plan === undefined || story === undefined) {
49
- return yield* unknownStory(storyId)
50
- }
51
- return { plan, story }
52
- })
53
-
54
- export const storyDetailOf = (
55
- sources: RunSources,
56
- stateDir: string,
57
- storyId: string
58
- ): Effect.Effect<StoryDetail, FlowError | UnknownStory> =>
59
- Effect.gen(function* () {
60
- const { story } = yield* storyOf(sources, stateDir, storyId)
61
- const stored = yield* loadStoryFiles(sources.files, stateDir, story)
62
- const epic = yield* loadEpicFiles(sources.files, stateDir)
63
- const item = epic.board?.items.find((candidate) => candidate.id === storyId)
64
- const verdictText = stored.verdict === undefined ? undefined : encodeJson(stored.verdict)
65
- const verdict =
66
- verdictText === undefined ? undefined : Option.getOrUndefined(decodeJson(verdictText))
67
- const gateDir = join(stateDir, "stories", storyId, "gates")
68
- return StoryDetail.make({
69
- id: story.id,
70
- title: story.title,
71
- ...(stored.state === undefined
72
- ? {}
73
- : {
74
- state: {
75
- status: stored.state.status,
76
- branch: stored.state.branch,
77
- worktree: stored.state.worktree,
78
- ...(stored.state.executor === undefined ? {} : { executor: stored.state.executor })
79
- }
80
- }),
81
- ...(item === undefined
82
- ? {}
83
- : {
84
- board: {
85
- status: item.status,
86
- ...(item.detail === undefined ? {} : { detail: item.detail })
87
- }
88
- }),
89
- tasks: (stored.plan?.tasks ?? []).map((task) => ({
90
- title: task.title,
91
- completed: task.completed
92
- })),
93
- ...(stored.findings === undefined ? {} : { findings: stored.findings }),
94
- ...(verdict === undefined ? {} : { verdict }),
95
- gateLogs: sources.listFiles(gateDir).map((name) => join(gateDir, name))
96
- })
97
- })
98
-
99
- export const storyDiagnosisOf = (
100
- sources: RunSources,
101
- args: {
102
- readonly stateDir: string
103
- readonly storyId: string
104
- readonly runId: string
105
- readonly tracePath: string
106
- readonly transcriptsDir: string | undefined
107
- readonly budget: number
108
- }
109
- ): Effect.Effect<Diagnosis, FlowError | UnknownStory | TraceUnreadable> =>
110
- Effect.gen(function* () {
111
- const { plan, story } = yield* storyOf(sources, args.stateDir, args.storyId)
112
- const trace = yield* readTrace(sources.files, args.tracePath).pipe(
113
- Effect.mapError((error) =>
114
- TraceUnreadable.make({ path: args.tracePath, message: describeFlowError(error) })
115
- )
116
- )
117
- const stored = yield* loadStoryFiles(sources.files, args.stateDir, story)
118
- const epic = yield* loadEpicFiles(sources.files, args.stateDir)
119
- const transcriptFiles: TranscriptFiles = {
120
- read: (path) => Effect.catch(sources.files.read(path), () => Effect.succeed(undefined)),
121
- list: sources.listFiles
122
- }
123
- const entries =
124
- args.transcriptsDir === undefined
125
- ? undefined
126
- : yield* loadTranscript(transcriptFiles, args.transcriptsDir, { lane: story.id })
127
- const inputs: RetroInputs = {
128
- runId: args.runId,
129
- plan,
130
- trace,
131
- transcripts: entries === undefined ? undefined : new Map([[story.id, entries]]),
132
- report: epic.report,
133
- board: epic.board,
134
- stories: [stored]
135
- }
136
- const digest = renderStoryDigest(inputs, story.id, args.budget) ?? ""
137
- const item = epic.board?.items.find((candidate) => candidate.id === story.id)
138
- const gates = yield* tailGateLogs(
139
- sources,
140
- join(args.stateDir, "stories", story.id, "gates"),
141
- args.budget - digest.length
142
- )
143
- const transcriptTail = yield* tailTranscript(
144
- sources,
145
- args.transcriptsDir,
146
- story.id,
147
- gates.remaining
148
- )
149
- return Diagnosis.make({
150
- runId: args.runId,
151
- story: story.id,
152
- digest,
153
- ...(item?.detail === undefined ? {} : { failure: { message: item.detail, lane: story.id } }),
154
- gateLogTails: gates.tails,
155
- transcriptTail
156
- })
157
- })