@llm4ts/shell 2.45.0 → 2.47.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/Cli.d.ts +3 -7
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +20 -4
- package/dist/Cli.js.map +1 -1
- package/dist/FlowLaunch.d.ts +2 -16
- package/dist/FlowLaunch.d.ts.map +1 -1
- package/dist/FlowLaunch.js +20 -66
- package/dist/FlowLaunch.js.map +1 -1
- package/dist/FlowProcess.d.ts +67 -0
- package/dist/FlowProcess.d.ts.map +1 -0
- package/dist/FlowProcess.js +175 -0
- package/dist/FlowProcess.js.map +1 -0
- package/dist/Menu.d.ts +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/OperatorActTools.d.ts +170 -0
- package/dist/OperatorActTools.d.ts.map +1 -0
- package/dist/OperatorActTools.js +332 -0
- package/dist/OperatorActTools.js.map +1 -0
- package/dist/OperatorConfig.d.ts +44 -0
- package/dist/OperatorConfig.d.ts.map +1 -0
- package/dist/OperatorConfig.js +88 -0
- package/dist/OperatorConfig.js.map +1 -0
- package/dist/OperatorNode.d.ts +45 -0
- package/dist/OperatorNode.d.ts.map +1 -0
- package/dist/OperatorNode.js +103 -0
- package/dist/OperatorNode.js.map +1 -0
- package/dist/OperatorRuns.d.ts +76 -0
- package/dist/OperatorRuns.d.ts.map +1 -0
- package/dist/OperatorRuns.js +146 -0
- package/dist/OperatorRuns.js.map +1 -0
- package/dist/OperatorServer.d.ts +160 -0
- package/dist/OperatorServer.d.ts.map +1 -0
- package/dist/OperatorServer.js +123 -0
- package/dist/OperatorServer.js.map +1 -0
- package/dist/OperatorTools.d.ts +558 -0
- package/dist/OperatorTools.d.ts.map +1 -0
- package/dist/OperatorTools.js +398 -0
- package/dist/OperatorTools.js.map +1 -0
- package/dist/Refine.d.ts +2 -2
- package/dist/Refine.d.ts.map +1 -1
- package/dist/Refine.js +3 -5
- package/dist/Refine.js.map +1 -1
- package/dist/RunSupervisor.d.ts +51 -0
- package/dist/RunSupervisor.d.ts.map +1 -0
- package/dist/RunSupervisor.js +282 -0
- package/dist/RunSupervisor.js.map +1 -0
- package/dist/ShellErrors.d.ts +9 -0
- package/dist/ShellErrors.d.ts.map +1 -0
- package/dist/ShellErrors.js +7 -0
- package/dist/ShellErrors.js.map +1 -0
- package/flows/epic-mvp.js +21 -0
- package/flows/lib/epic-mvp.js +451 -0
- package/flows/lib/epic-stories.js +88 -48
- package/flows/port-guide.js +4 -1
- package/package.json +13 -4
- package/src/Cli.ts +64 -3
- package/src/FlowLaunch.ts +20 -66
- package/src/FlowProcess.ts +239 -0
- package/src/OperatorActTools.ts +407 -0
- package/src/OperatorConfig.ts +148 -0
- package/src/OperatorNode.ts +147 -0
- package/src/OperatorRuns.ts +270 -0
- package/src/OperatorServer.ts +192 -0
- package/src/OperatorTools.ts +610 -0
- package/src/Refine.ts +4 -7
- package/src/RunSupervisor.ts +403 -0
- package/src/ShellErrors.ts +6 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import * as Duration from "effect/Duration"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Schema from "effect/Schema"
|
|
5
|
+
import * as Tool from "effect/ai/Tool"
|
|
6
|
+
import * as Toolkit from "effect/ai/Toolkit"
|
|
7
|
+
import { Board, BoardStatus, BoardVersion } from "@llm4ts/flow/BoardSync"
|
|
8
|
+
import { CostLedgerFileName } from "@llm4ts/flow/CostLedger"
|
|
9
|
+
import { buildCostReport } from "@llm4ts/flow/CostReport"
|
|
10
|
+
import { describeFlowError, type FlowError } from "@llm4ts/flow/FlowError"
|
|
11
|
+
import { TraceLine } from "@llm4ts/flow/FlowRecorder"
|
|
12
|
+
import {
|
|
13
|
+
CostsOut,
|
|
14
|
+
Diagnosis,
|
|
15
|
+
OperatorRun,
|
|
16
|
+
PreflightResult,
|
|
17
|
+
ReadFailed,
|
|
18
|
+
RunStatus,
|
|
19
|
+
StoryDetail,
|
|
20
|
+
UnknownEpic,
|
|
21
|
+
UnknownRepo,
|
|
22
|
+
UnknownRun,
|
|
23
|
+
UnknownStory,
|
|
24
|
+
type ConnectorHealthOut,
|
|
25
|
+
type ExecutorHealthOut
|
|
26
|
+
} from "@llm4ts/flow/OperatorSchemas"
|
|
27
|
+
import { loadVersioned } from "@llm4ts/flow/Persistence"
|
|
28
|
+
import { defaultPlanPath } from "@llm4ts/flow/Plan"
|
|
29
|
+
import { readRetroIndex, retroApprovalOf, retroPaths } from "@llm4ts/flow/Retro"
|
|
30
|
+
import { rolesOf } from "@llm4ts/flow/Roster"
|
|
31
|
+
import { makeStoryPlanStore } from "@llm4ts/flow/StoryPlan"
|
|
32
|
+
import { emptyTree, treeInputsOfTrace } from "@llm4ts/runner/AgentTree"
|
|
33
|
+
import { CostsResult, makeCostsProgram } from "@llm4ts/runner/Costs"
|
|
34
|
+
import type { DoctorReport } from "@llm4ts/runner/Doctor"
|
|
35
|
+
import type { RosterReport } from "@llm4ts/runner/ExecutorRoster"
|
|
36
|
+
import { ProfileReport, profileOf } from "@llm4ts/runner/Profile"
|
|
37
|
+
import {
|
|
38
|
+
TraceUnreadable,
|
|
39
|
+
costsOfRun,
|
|
40
|
+
eventsAfter,
|
|
41
|
+
foldTrace,
|
|
42
|
+
runDiagnosisOf,
|
|
43
|
+
runStatusOf,
|
|
44
|
+
type RunSources
|
|
45
|
+
} from "@llm4ts/runner/RunReaders"
|
|
46
|
+
import { storyDetailOf, storyDiagnosisOf } from "@llm4ts/runner/StoryReaders"
|
|
47
|
+
import { loadTranscript, tailLinesOf, type TranscriptFiles } from "@llm4ts/runner/Transcripts"
|
|
48
|
+
import type { DiscoveredFlow } from "./FlowCatalog.ts"
|
|
49
|
+
import type { OperatorConfig } from "./OperatorConfig.ts"
|
|
50
|
+
import {
|
|
51
|
+
discoveredRuns,
|
|
52
|
+
epicDirOf,
|
|
53
|
+
epicOfRun,
|
|
54
|
+
epicsDir,
|
|
55
|
+
readEpicRunsStrict,
|
|
56
|
+
readFailed,
|
|
57
|
+
resolveRun,
|
|
58
|
+
runTraceLines,
|
|
59
|
+
stateRoot,
|
|
60
|
+
tracePending,
|
|
61
|
+
transcriptsDir,
|
|
62
|
+
type RunLookupDeps
|
|
63
|
+
} from "./OperatorRuns.ts"
|
|
64
|
+
import { packageVersion } from "./Package.ts"
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The operator's read-only MCP tools (`llm4ts mcp`, ADR 0041): answers about
|
|
68
|
+
* repositories, epics and runs from the files they write, over an
|
|
69
|
+
* `OperatorDeps` record so tests call the handlers directly on memory stores
|
|
70
|
+
* and fakes. A client never names a path: repositories, epics and runs are
|
|
71
|
+
* ids resolved against the configuration and the files found on disk
|
|
72
|
+
* (`OperatorRuns.ts`).
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
export interface OperatorDeps extends RunLookupDeps {
|
|
76
|
+
readonly config: OperatorConfig
|
|
77
|
+
/** Relative paths of every regular file under a directory, recursively; none when absent. */
|
|
78
|
+
readonly listFiles: (directory: string) => ReadonlyArray<string>
|
|
79
|
+
readonly transcripts: TranscriptFiles
|
|
80
|
+
readonly doctor: Effect.Effect<DoctorReport>
|
|
81
|
+
readonly roster: (repoPath: string) => Effect.Effect<RosterReport, FlowError>
|
|
82
|
+
readonly probe: (url: string) => Effect.Effect<boolean>
|
|
83
|
+
readonly flows: (repoPath: string) => Effect.Effect<ReadonlyArray<DiscoveredFlow>, unknown>
|
|
84
|
+
readonly now: Effect.Effect<number>
|
|
85
|
+
readonly environment: Readonly<Record<string, string | undefined>>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The repository a tool call means; the id may be left out when the server serves one. */
|
|
89
|
+
export const resolveRepo = (
|
|
90
|
+
config: OperatorConfig,
|
|
91
|
+
id: string | undefined
|
|
92
|
+
): Effect.Effect<{ id: string; path: string }, UnknownRepo> => {
|
|
93
|
+
const ids = config.repos.map((repo) => repo.id).join(", ")
|
|
94
|
+
if (id === undefined) {
|
|
95
|
+
const only = config.repos.length === 1 ? config.repos[0] : undefined
|
|
96
|
+
return only === undefined
|
|
97
|
+
? Effect.fail(
|
|
98
|
+
UnknownRepo.make({ message: `repo is required: this server serves ${ids || "none"}` })
|
|
99
|
+
)
|
|
100
|
+
: Effect.succeed({ id: only.id, path: only.path })
|
|
101
|
+
}
|
|
102
|
+
const repo = config.repos.find((candidate) => candidate.id === id)
|
|
103
|
+
return repo === undefined
|
|
104
|
+
? Effect.fail(UnknownRepo.make({ message: `unknown repo '${id}': this server serves ${ids}` }))
|
|
105
|
+
: Effect.succeed({ id: repo.id, path: repo.path })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A state file that exists but does not parse, named in the message. */
|
|
109
|
+
const readFailedAt =
|
|
110
|
+
(path: string) =>
|
|
111
|
+
(error: unknown): ReadFailed => {
|
|
112
|
+
const message = describeFlowError(error)
|
|
113
|
+
return ReadFailed.make({ message: message.includes(path) ? message : `${path}: ${message}` })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sourcesOf = (deps: OperatorDeps): RunSources => ({
|
|
117
|
+
files: deps.files,
|
|
118
|
+
listFiles: deps.listFiles
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const lastLines = (text: string | undefined, count: number): ReadonlyArray<string> => {
|
|
122
|
+
if (text === undefined || count <= 0) return []
|
|
123
|
+
const rows = text.replace(/\r?\n$/u, "").split(/\r?\n/u)
|
|
124
|
+
return rows.length === 1 && rows[0] === "" ? [] : rows.slice(-count)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const clamp = (value: number, low: number, high: number): number =>
|
|
128
|
+
Math.min(high, Math.max(low, value))
|
|
129
|
+
|
|
130
|
+
// ---- Tools --------------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
const repoParam = Schema.optional(Schema.String)
|
|
133
|
+
|
|
134
|
+
export const preflight = Tool.make("preflight", {
|
|
135
|
+
description:
|
|
136
|
+
"Is this machine ready to run? Connector health (probe 'auth' is the CLI's own login check; 'version' only ran `<cli> --version` and does not prove a live session), roster executors with their health URL probed, Node and llm4ts versions.",
|
|
137
|
+
parameters: Schema.Struct({ repo: repoParam }),
|
|
138
|
+
success: PreflightResult,
|
|
139
|
+
failure: Schema.Union([UnknownRepo])
|
|
140
|
+
})
|
|
141
|
+
.annotate(Tool.Readonly, true)
|
|
142
|
+
.annotate(Tool.Destructive, false)
|
|
143
|
+
|
|
144
|
+
export const FlowOut = Schema.Struct({
|
|
145
|
+
name: Schema.String,
|
|
146
|
+
tier: Schema.String,
|
|
147
|
+
description: Schema.optionalKey(Schema.String),
|
|
148
|
+
kit: Schema.optionalKey(Schema.String)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
export const list_flows = Tool.make("list_flows", {
|
|
152
|
+
description: "Flows that can be started, with their tier (project, global, builtin).",
|
|
153
|
+
parameters: Schema.Struct({ repo: repoParam }),
|
|
154
|
+
success: Schema.Array(FlowOut),
|
|
155
|
+
failure: Schema.Union([UnknownRepo, ReadFailed])
|
|
156
|
+
})
|
|
157
|
+
.annotate(Tool.Readonly, true)
|
|
158
|
+
.annotate(Tool.Destructive, false)
|
|
159
|
+
|
|
160
|
+
export const EpicOut = Schema.Struct({
|
|
161
|
+
id: Schema.String,
|
|
162
|
+
title: Schema.String,
|
|
163
|
+
stories: Schema.Int,
|
|
164
|
+
board: Schema.Array(Schema.Struct({ id: Schema.String, status: BoardStatus })),
|
|
165
|
+
lastRun: Schema.optionalKey(
|
|
166
|
+
Schema.Struct({ runId: Schema.String, startedAt: Schema.Number, action: Schema.String })
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
export const list_epics = Tool.make("list_epics", {
|
|
171
|
+
description: "Epics under .llm4ts/epics/ with title, story count, board and last run.",
|
|
172
|
+
parameters: Schema.Struct({ repo: repoParam }),
|
|
173
|
+
success: Schema.Array(EpicOut),
|
|
174
|
+
failure: Schema.Union([UnknownRepo, ReadFailed])
|
|
175
|
+
})
|
|
176
|
+
.annotate(Tool.Readonly, true)
|
|
177
|
+
.annotate(Tool.Destructive, false)
|
|
178
|
+
|
|
179
|
+
export const list_runs = Tool.make("list_runs", {
|
|
180
|
+
description:
|
|
181
|
+
"Runs this server started plus runs found in traces, newest first (default 20). A run found only in a trace has flow '?'.",
|
|
182
|
+
parameters: Schema.Struct({
|
|
183
|
+
repo: repoParam,
|
|
184
|
+
epic: Schema.optional(Schema.String),
|
|
185
|
+
limit: Schema.optional(Schema.Int)
|
|
186
|
+
}),
|
|
187
|
+
success: Schema.Array(OperatorRun),
|
|
188
|
+
failure: Schema.Union([UnknownRepo, UnknownEpic, ReadFailed])
|
|
189
|
+
})
|
|
190
|
+
.annotate(Tool.Readonly, true)
|
|
191
|
+
.annotate(Tool.Destructive, false)
|
|
192
|
+
|
|
193
|
+
export const run_status = Tool.make("run_status", {
|
|
194
|
+
description:
|
|
195
|
+
"One run now: state, elapsed, lanes, stories board, tokens and cost so far, warnings, and a one-line summary.",
|
|
196
|
+
parameters: Schema.Struct({ runId: Schema.String }),
|
|
197
|
+
success: RunStatus,
|
|
198
|
+
failure: Schema.Union([UnknownRun, TraceUnreadable, ReadFailed])
|
|
199
|
+
})
|
|
200
|
+
.annotate(Tool.Readonly, true)
|
|
201
|
+
.annotate(Tool.Destructive, false)
|
|
202
|
+
|
|
203
|
+
export const run_events = Tool.make("run_events", {
|
|
204
|
+
description:
|
|
205
|
+
"Raw trace lines after sinceSeq, optionally only the given kinds; limit defaults to 200, at most 1000.",
|
|
206
|
+
parameters: Schema.Struct({
|
|
207
|
+
runId: Schema.String,
|
|
208
|
+
sinceSeq: Schema.optional(Schema.Int),
|
|
209
|
+
kinds: Schema.optional(Schema.Array(Schema.String)),
|
|
210
|
+
limit: Schema.optional(Schema.Int)
|
|
211
|
+
}),
|
|
212
|
+
success: Schema.Array(TraceLine),
|
|
213
|
+
failure: Schema.Union([UnknownRun, TraceUnreadable, ReadFailed])
|
|
214
|
+
})
|
|
215
|
+
.annotate(Tool.Readonly, true)
|
|
216
|
+
.annotate(Tool.Destructive, false)
|
|
217
|
+
|
|
218
|
+
export const RunLogOut = Schema.Struct({
|
|
219
|
+
stdout: Schema.Array(Schema.String),
|
|
220
|
+
stderr: Schema.Array(Schema.String)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
export const run_log = Tool.make("run_log", {
|
|
224
|
+
description:
|
|
225
|
+
"The last lines (default 100) of a run's stdout and stderr; only runs this server started have logs.",
|
|
226
|
+
parameters: Schema.Struct({ runId: Schema.String, lines: Schema.optional(Schema.Int) }),
|
|
227
|
+
success: RunLogOut,
|
|
228
|
+
failure: Schema.Union([UnknownRun, ReadFailed])
|
|
229
|
+
})
|
|
230
|
+
.annotate(Tool.Readonly, true)
|
|
231
|
+
.annotate(Tool.Destructive, false)
|
|
232
|
+
|
|
233
|
+
export const story_detail = Tool.make("story_detail", {
|
|
234
|
+
description:
|
|
235
|
+
"One story of an epic: durable state, board entry, plan tasks, findings, last judge verdict, gate logs.",
|
|
236
|
+
parameters: Schema.Struct({ repo: repoParam, epic: Schema.String, story: Schema.String }),
|
|
237
|
+
success: StoryDetail,
|
|
238
|
+
failure: Schema.Union([UnknownRepo, UnknownEpic, UnknownStory, ReadFailed])
|
|
239
|
+
})
|
|
240
|
+
.annotate(Tool.Readonly, true)
|
|
241
|
+
.annotate(Tool.Destructive, false)
|
|
242
|
+
|
|
243
|
+
export const diagnose = Tool.make("diagnose", {
|
|
244
|
+
description:
|
|
245
|
+
"Why a run or one of its stories failed: a digest, the failure, gate log tails and the transcript tail, within a character budget (default 12000). A story needs an epic run.",
|
|
246
|
+
parameters: Schema.Struct({
|
|
247
|
+
runId: Schema.String,
|
|
248
|
+
story: Schema.optional(Schema.String),
|
|
249
|
+
chars: Schema.optional(Schema.Int)
|
|
250
|
+
}),
|
|
251
|
+
success: Diagnosis,
|
|
252
|
+
failure: Schema.Union([UnknownRun, UnknownStory, TraceUnreadable, ReadFailed])
|
|
253
|
+
})
|
|
254
|
+
.annotate(Tool.Readonly, true)
|
|
255
|
+
.annotate(Tool.Destructive, false)
|
|
256
|
+
|
|
257
|
+
export const transcript_tail = Tool.make("transcript_tail", {
|
|
258
|
+
description:
|
|
259
|
+
"The last rows (default 40) of a lane's transcript, optionally one role; empty when the run kept none.",
|
|
260
|
+
parameters: Schema.Struct({
|
|
261
|
+
runId: Schema.String,
|
|
262
|
+
lane: Schema.String,
|
|
263
|
+
entries: Schema.optional(Schema.Int),
|
|
264
|
+
role: Schema.optional(Schema.String)
|
|
265
|
+
}),
|
|
266
|
+
success: Schema.Array(Schema.String),
|
|
267
|
+
failure: Schema.Union([UnknownRun, ReadFailed])
|
|
268
|
+
})
|
|
269
|
+
.annotate(Tool.Readonly, true)
|
|
270
|
+
.annotate(Tool.Destructive, false)
|
|
271
|
+
|
|
272
|
+
export const costs = Tool.make("costs", {
|
|
273
|
+
description:
|
|
274
|
+
"What one run cost (trace total, the ledger's figure and whether they differ), or the repository's costs over every trace when runId is left out.",
|
|
275
|
+
parameters: Schema.Struct({ repo: repoParam, runId: Schema.optional(Schema.String) }),
|
|
276
|
+
success: Schema.Union([CostsOut, CostsResult]),
|
|
277
|
+
failure: Schema.Union([UnknownRepo, UnknownRun, TraceUnreadable, ReadFailed])
|
|
278
|
+
})
|
|
279
|
+
.annotate(Tool.Readonly, true)
|
|
280
|
+
.annotate(Tool.Destructive, false)
|
|
281
|
+
|
|
282
|
+
export const profile = Tool.make("profile", {
|
|
283
|
+
description: "Where a run's time went: per story, model, gate and tool.",
|
|
284
|
+
parameters: Schema.Struct({ runId: Schema.String }),
|
|
285
|
+
success: ProfileReport,
|
|
286
|
+
failure: Schema.Union([UnknownRun, TraceUnreadable, ReadFailed])
|
|
287
|
+
})
|
|
288
|
+
.annotate(Tool.Readonly, true)
|
|
289
|
+
.annotate(Tool.Destructive, false)
|
|
290
|
+
|
|
291
|
+
export const RetroOut = Schema.Struct({
|
|
292
|
+
runId: Schema.String,
|
|
293
|
+
at: Schema.Number,
|
|
294
|
+
approved: Schema.Boolean,
|
|
295
|
+
applied: Schema.Boolean
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
export const retro_list = Tool.make("retro_list", {
|
|
299
|
+
description: "Retros written for an epic, oldest first, with their approval state.",
|
|
300
|
+
parameters: Schema.Struct({ repo: repoParam, epic: Schema.String }),
|
|
301
|
+
success: Schema.Array(RetroOut),
|
|
302
|
+
failure: Schema.Union([UnknownRepo, UnknownEpic])
|
|
303
|
+
})
|
|
304
|
+
.annotate(Tool.Readonly, true)
|
|
305
|
+
.annotate(Tool.Destructive, false)
|
|
306
|
+
|
|
307
|
+
export const readToolkit = Toolkit.make(
|
|
308
|
+
preflight,
|
|
309
|
+
list_flows,
|
|
310
|
+
list_epics,
|
|
311
|
+
list_runs,
|
|
312
|
+
run_status,
|
|
313
|
+
run_events,
|
|
314
|
+
run_log,
|
|
315
|
+
story_detail,
|
|
316
|
+
diagnose,
|
|
317
|
+
transcript_tail,
|
|
318
|
+
costs,
|
|
319
|
+
profile,
|
|
320
|
+
retro_list
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
// ---- Handlers -----------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
const defaultRunLimit = 20
|
|
326
|
+
const defaultEventLimit = 200
|
|
327
|
+
const maxEventLimit = 1_000
|
|
328
|
+
const defaultLogLines = 100
|
|
329
|
+
const defaultDiagnoseChars = 12_000
|
|
330
|
+
const defaultTranscriptEntries = 40
|
|
331
|
+
const transcriptWidth = 160
|
|
332
|
+
|
|
333
|
+
const isExcluded = (report: RosterReport, id: string, now: number): boolean =>
|
|
334
|
+
report.exclusions.some(
|
|
335
|
+
(exclusion) =>
|
|
336
|
+
exclusion.id === id && (exclusion.kind !== "until" || (exclusion.until ?? 0) > now)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
const executorsOf = (
|
|
340
|
+
deps: OperatorDeps,
|
|
341
|
+
report: RosterReport
|
|
342
|
+
): Effect.Effect<ReadonlyArray<ExecutorHealthOut>> =>
|
|
343
|
+
Effect.gen(function* () {
|
|
344
|
+
const now = yield* deps.now
|
|
345
|
+
return yield* Effect.forEach(report.document?.executors ?? [], (spec) =>
|
|
346
|
+
Effect.gen(function* () {
|
|
347
|
+
const healthy = spec.health === undefined ? undefined : yield* deps.probe(spec.health)
|
|
348
|
+
return {
|
|
349
|
+
id: spec.id,
|
|
350
|
+
harness: spec.harness,
|
|
351
|
+
roles: rolesOf(spec),
|
|
352
|
+
disabled: spec.disabled === true || isExcluded(report, spec.id, now),
|
|
353
|
+
...(healthy === undefined ? {} : { healthy })
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const firstLine = (text: string): string => text.split("\n")[0]?.trim() ?? ""
|
|
360
|
+
|
|
361
|
+
export const readHandlers = (
|
|
362
|
+
deps: OperatorDeps
|
|
363
|
+
): Toolkit.HandlersFrom<Toolkit.Tools<typeof readToolkit>> =>
|
|
364
|
+
readToolkit.of({
|
|
365
|
+
preflight: ({ repo: repoId }) =>
|
|
366
|
+
Effect.gen(function* () {
|
|
367
|
+
const repo = yield* resolveRepo(deps.config, repoId)
|
|
368
|
+
const doctor = yield* deps.doctor
|
|
369
|
+
const connectors = doctor.connectors.map(
|
|
370
|
+
(connector): ConnectorHealthOut => ({
|
|
371
|
+
id: connector.id,
|
|
372
|
+
availability: connector.status.availability,
|
|
373
|
+
authStatus: connector.status.authStatus,
|
|
374
|
+
probe: connector.probe,
|
|
375
|
+
...(connector.status.latency === undefined
|
|
376
|
+
? {}
|
|
377
|
+
: { latencyMs: Math.round(Duration.toMillis(connector.status.latency)) })
|
|
378
|
+
})
|
|
379
|
+
)
|
|
380
|
+
const roster = yield* deps.roster(repo.path).pipe(
|
|
381
|
+
Effect.map((report) => ({ report, note: undefined })),
|
|
382
|
+
Effect.catch((error) =>
|
|
383
|
+
Effect.succeed({ report: undefined, note: `roster: ${describeFlowError(error)}` })
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
const executors = roster.report === undefined ? [] : yield* executorsOf(deps, roster.report)
|
|
387
|
+
return PreflightResult.make({
|
|
388
|
+
llm4ts: packageVersion,
|
|
389
|
+
node: process.version,
|
|
390
|
+
connectors,
|
|
391
|
+
executors,
|
|
392
|
+
notes: [...doctor.lines, ...(roster.note === undefined ? [] : [roster.note])]
|
|
393
|
+
})
|
|
394
|
+
}),
|
|
395
|
+
|
|
396
|
+
list_flows: ({ repo: repoId }) =>
|
|
397
|
+
Effect.gen(function* () {
|
|
398
|
+
const repo = yield* resolveRepo(deps.config, repoId)
|
|
399
|
+
const flows = yield* deps.flows(repo.path).pipe(Effect.mapError(readFailed))
|
|
400
|
+
return flows.map((flow) => ({
|
|
401
|
+
name: flow.name,
|
|
402
|
+
tier: flow.tier,
|
|
403
|
+
...(flow.description === undefined ? {} : { description: flow.description }),
|
|
404
|
+
...(flow.kit === undefined ? {} : { kit: flow.kit })
|
|
405
|
+
}))
|
|
406
|
+
}),
|
|
407
|
+
|
|
408
|
+
list_epics: ({ repo: repoId }) =>
|
|
409
|
+
Effect.gen(function* () {
|
|
410
|
+
const repo = yield* resolveRepo(deps.config, repoId)
|
|
411
|
+
return yield* Effect.forEach(deps.listEpics(epicsDir(repo)), (id) =>
|
|
412
|
+
Effect.gen(function* () {
|
|
413
|
+
const dir = join(epicsDir(repo), id)
|
|
414
|
+
const planPath = join(dir, "plan.md")
|
|
415
|
+
const plan = yield* makeStoryPlanStore(deps.files)
|
|
416
|
+
.load(planPath)
|
|
417
|
+
.pipe(Effect.mapError(readFailedAt(planPath)))
|
|
418
|
+
const boardPath = join(dir, "board.json")
|
|
419
|
+
const board = yield* loadVersioned(deps.files, boardPath, BoardVersion, Board).pipe(
|
|
420
|
+
Effect.mapError(readFailedAt(boardPath))
|
|
421
|
+
)
|
|
422
|
+
const runs = yield* readEpicRunsStrict(deps.files, dir)
|
|
423
|
+
const last = runs[runs.length - 1]
|
|
424
|
+
const planTitle = plan === undefined ? "" : firstLine(plan.epic)
|
|
425
|
+
return {
|
|
426
|
+
id,
|
|
427
|
+
title: planTitle.length > 0 ? planTitle : (board?.title ?? id),
|
|
428
|
+
stories: plan?.stories.length ?? board?.items.length ?? 0,
|
|
429
|
+
board: (board?.items ?? []).map((item) => ({ id: item.id, status: item.status })),
|
|
430
|
+
...(last === undefined
|
|
431
|
+
? {}
|
|
432
|
+
: {
|
|
433
|
+
lastRun: { runId: last.runId, startedAt: last.startedAt, action: last.action }
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
)
|
|
438
|
+
}),
|
|
439
|
+
|
|
440
|
+
list_runs: ({ repo: repoId, epic, limit }) =>
|
|
441
|
+
Effect.gen(function* () {
|
|
442
|
+
const repo = yield* resolveRepo(deps.config, repoId)
|
|
443
|
+
const supervised = yield* deps.supervisor.list(repo.id)
|
|
444
|
+
const discovered = yield* discoveredRuns(
|
|
445
|
+
deps,
|
|
446
|
+
repo,
|
|
447
|
+
new Set(supervised.map((run) => run.runId))
|
|
448
|
+
)
|
|
449
|
+
let runs = [...supervised, ...discovered]
|
|
450
|
+
if (epic !== undefined) {
|
|
451
|
+
const dir = yield* epicDirOf(deps, repo, epic)
|
|
452
|
+
const epicRuns = yield* readEpicRunsStrict(deps.files, dir)
|
|
453
|
+
const ids = new Set(epicRuns.map((run) => run.runId))
|
|
454
|
+
runs = runs.filter((run) => run.knobs.epic === epic || ids.has(run.runId))
|
|
455
|
+
}
|
|
456
|
+
return runs
|
|
457
|
+
.sort((a, b) => b.startedAt - a.startedAt)
|
|
458
|
+
.slice(0, Math.max(0, limit ?? defaultRunLimit))
|
|
459
|
+
}),
|
|
460
|
+
|
|
461
|
+
run_status: ({ runId }) =>
|
|
462
|
+
Effect.gen(function* () {
|
|
463
|
+
const run = yield* resolveRun(deps, runId)
|
|
464
|
+
const epic = yield* epicOfRun(deps, run)
|
|
465
|
+
const fold = (yield* tracePending(deps.files, run))
|
|
466
|
+
? { lines: [], state: emptyTree(), seq: 0 }
|
|
467
|
+
: yield* foldTrace(deps.files, run.tracePath, epic?.dir)
|
|
468
|
+
return runStatusOf(fold, run, yield* deps.now)
|
|
469
|
+
}),
|
|
470
|
+
|
|
471
|
+
run_events: ({ runId, sinceSeq, kinds, limit }) =>
|
|
472
|
+
Effect.gen(function* () {
|
|
473
|
+
const run = yield* resolveRun(deps, runId)
|
|
474
|
+
const lines = yield* runTraceLines(deps.files, run)
|
|
475
|
+
return eventsAfter(
|
|
476
|
+
lines,
|
|
477
|
+
sinceSeq ?? 0,
|
|
478
|
+
kinds,
|
|
479
|
+
clamp(limit ?? defaultEventLimit, 0, maxEventLimit)
|
|
480
|
+
)
|
|
481
|
+
}),
|
|
482
|
+
|
|
483
|
+
run_log: ({ runId, lines }) =>
|
|
484
|
+
Effect.gen(function* () {
|
|
485
|
+
const run = yield* resolveRun(deps, runId)
|
|
486
|
+
const logDir = run.supervised?.logDir
|
|
487
|
+
if (logDir === undefined || logDir.length === 0) {
|
|
488
|
+
return { stdout: [], stderr: [] }
|
|
489
|
+
}
|
|
490
|
+
const count = lines ?? defaultLogLines
|
|
491
|
+
const stdout = yield* deps.files
|
|
492
|
+
.read(join(logDir, "stdout.log"))
|
|
493
|
+
.pipe(Effect.mapError(readFailed))
|
|
494
|
+
const stderr = yield* deps.files
|
|
495
|
+
.read(join(logDir, "stderr.log"))
|
|
496
|
+
.pipe(Effect.mapError(readFailed))
|
|
497
|
+
return { stdout: lastLines(stdout, count), stderr: lastLines(stderr, count) }
|
|
498
|
+
}),
|
|
499
|
+
|
|
500
|
+
story_detail: ({ repo: repoId, epic, story }) =>
|
|
501
|
+
Effect.gen(function* () {
|
|
502
|
+
const repo = yield* resolveRepo(deps.config, repoId)
|
|
503
|
+
const dir = yield* epicDirOf(deps, repo, epic)
|
|
504
|
+
return yield* storyDetailOf(sourcesOf(deps), dir, story).pipe(
|
|
505
|
+
Effect.mapError((error) => (error instanceof UnknownStory ? error : readFailed(error)))
|
|
506
|
+
)
|
|
507
|
+
}),
|
|
508
|
+
|
|
509
|
+
diagnose: ({ runId, story, chars }) =>
|
|
510
|
+
Effect.gen(function* () {
|
|
511
|
+
const run = yield* resolveRun(deps, runId)
|
|
512
|
+
const budget = chars ?? defaultDiagnoseChars
|
|
513
|
+
const transcripts = transcriptsDir(run.repo, run.runId)
|
|
514
|
+
if (story !== undefined) {
|
|
515
|
+
const epic = yield* epicOfRun(deps, run)
|
|
516
|
+
if (epic === undefined) {
|
|
517
|
+
return yield* UnknownStory.make({
|
|
518
|
+
message: `run ${runId} is not an epic run, so it has no story '${story}'`
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
return yield* storyDiagnosisOf(sourcesOf(deps), {
|
|
522
|
+
stateDir: epic.dir,
|
|
523
|
+
storyId: story,
|
|
524
|
+
runId: run.runId,
|
|
525
|
+
tracePath: run.tracePath,
|
|
526
|
+
transcriptsDir: transcripts,
|
|
527
|
+
budget
|
|
528
|
+
}).pipe(
|
|
529
|
+
Effect.mapError((error) =>
|
|
530
|
+
error instanceof UnknownStory || error instanceof TraceUnreadable
|
|
531
|
+
? error
|
|
532
|
+
: readFailed(error)
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
const task = run.supervised?.knobs.task
|
|
537
|
+
return yield* runDiagnosisOf(sourcesOf(deps), {
|
|
538
|
+
runId: run.runId,
|
|
539
|
+
tracePath: run.tracePath,
|
|
540
|
+
planPath: task === undefined ? undefined : join(run.repo.path, defaultPlanPath(task)),
|
|
541
|
+
transcriptsDir: transcripts,
|
|
542
|
+
gateLogDir: undefined,
|
|
543
|
+
budget
|
|
544
|
+
})
|
|
545
|
+
}),
|
|
546
|
+
|
|
547
|
+
transcript_tail: ({ runId, lane, entries, role }) =>
|
|
548
|
+
Effect.gen(function* () {
|
|
549
|
+
const run = yield* resolveRun(deps, runId)
|
|
550
|
+
const loaded = yield* loadTranscript(
|
|
551
|
+
deps.transcripts,
|
|
552
|
+
transcriptsDir(run.repo, run.runId),
|
|
553
|
+
{ lane }
|
|
554
|
+
)
|
|
555
|
+
return (
|
|
556
|
+
tailLinesOf(
|
|
557
|
+
loaded,
|
|
558
|
+
{ lane },
|
|
559
|
+
role,
|
|
560
|
+
transcriptWidth,
|
|
561
|
+
Math.max(0, entries ?? defaultTranscriptEntries)
|
|
562
|
+
) ?? []
|
|
563
|
+
)
|
|
564
|
+
}),
|
|
565
|
+
|
|
566
|
+
costs: ({ repo: repoId, runId }) =>
|
|
567
|
+
Effect.gen(function* () {
|
|
568
|
+
if (runId !== undefined) {
|
|
569
|
+
const run = yield* resolveRun(deps, runId)
|
|
570
|
+
if (yield* tracePending(deps.files, run)) {
|
|
571
|
+
return CostsOut.make({ report: buildCostReport([]), total: 0, differs: false })
|
|
572
|
+
}
|
|
573
|
+
return yield* costsOfRun(
|
|
574
|
+
deps.files,
|
|
575
|
+
run.tracePath,
|
|
576
|
+
join(stateRoot(run.repo), CostLedgerFileName),
|
|
577
|
+
run.runId
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
const repo = yield* resolveRepo(deps.config, repoId)
|
|
581
|
+
return yield* makeCostsProgram(
|
|
582
|
+
{ repos: [repo.path] },
|
|
583
|
+
{ files: deps.files, listTraces: deps.listTraces }
|
|
584
|
+
)
|
|
585
|
+
}),
|
|
586
|
+
|
|
587
|
+
profile: ({ runId }) =>
|
|
588
|
+
Effect.gen(function* () {
|
|
589
|
+
const run = yield* resolveRun(deps, runId)
|
|
590
|
+
const lines = yield* runTraceLines(deps.files, run)
|
|
591
|
+
return profileOf(treeInputsOfTrace(lines))
|
|
592
|
+
}),
|
|
593
|
+
|
|
594
|
+
retro_list: ({ repo: repoId, epic }) =>
|
|
595
|
+
Effect.gen(function* () {
|
|
596
|
+
const repo = yield* resolveRepo(deps.config, repoId)
|
|
597
|
+
const dir = yield* epicDirOf(deps, repo, epic)
|
|
598
|
+
const index = yield* readRetroIndex(deps.files, dir)
|
|
599
|
+
return yield* Effect.forEach(index, (entry) =>
|
|
600
|
+
deps.files.read(retroPaths(dir, entry.runId).report).pipe(
|
|
601
|
+
Effect.catch(() => Effect.succeed(undefined)),
|
|
602
|
+
Effect.map((markdown) => ({
|
|
603
|
+
runId: entry.runId,
|
|
604
|
+
at: entry.at,
|
|
605
|
+
...retroApprovalOf(markdown ?? "")
|
|
606
|
+
}))
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
})
|
|
610
|
+
})
|
package/src/Refine.ts
CHANGED
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
scenarioTitles
|
|
16
16
|
} from "@llm4ts/flow/Decisions"
|
|
17
17
|
import { Domains, parseDomains, renderDomains } from "@llm4ts/flow/Domains"
|
|
18
|
-
import {
|
|
18
|
+
import { approveMarkdown } from "@llm4ts/flow/Approval"
|
|
19
|
+
|
|
20
|
+
// Kept on the published `@llm4ts/shell/Refine` subpath.
|
|
21
|
+
export { approveMarkdown }
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* The interactive front of `modernize-refine` (ADR 0015). The FILE is the
|
|
@@ -81,12 +84,6 @@ export const saveDomains = (modDir: string, domains: Domains) =>
|
|
|
81
84
|
fs.writeFileString(`${modDir}/domains.md`, renderDomains(domains))
|
|
82
85
|
)
|
|
83
86
|
|
|
84
|
-
/** `- [ ] Approved` → `- [x] Approved` in a markdown document; unchanged when already approved. */
|
|
85
|
-
export const approveMarkdown = (markdown: string): string =>
|
|
86
|
-
markdown.includes(ApprovedMarker)
|
|
87
|
-
? markdown
|
|
88
|
-
: markdown.replace(DraftApprovalMarker, ApprovedMarker)
|
|
89
|
-
|
|
90
87
|
/** The decision signature: LLM4TS_APPROVER, else the OS user. */
|
|
91
88
|
export const signer = (environment: Readonly<Record<string, string | undefined>>): string => {
|
|
92
89
|
const approver = environment.LLM4TS_APPROVER?.trim()
|