@llm4ts/shell 2.44.1 → 2.46.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 (70) hide show
  1. package/dist/Cli.d.ts +3 -7
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +20 -4
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/FlowLaunch.d.ts +2 -16
  6. package/dist/FlowLaunch.d.ts.map +1 -1
  7. package/dist/FlowLaunch.js +20 -66
  8. package/dist/FlowLaunch.js.map +1 -1
  9. package/dist/FlowProcess.d.ts +67 -0
  10. package/dist/FlowProcess.d.ts.map +1 -0
  11. package/dist/FlowProcess.js +175 -0
  12. package/dist/FlowProcess.js.map +1 -0
  13. package/dist/Menu.d.ts +1 -1
  14. package/dist/Menu.d.ts.map +1 -1
  15. package/dist/OperatorActTools.d.ts +170 -0
  16. package/dist/OperatorActTools.d.ts.map +1 -0
  17. package/dist/OperatorActTools.js +332 -0
  18. package/dist/OperatorActTools.js.map +1 -0
  19. package/dist/OperatorConfig.d.ts +44 -0
  20. package/dist/OperatorConfig.d.ts.map +1 -0
  21. package/dist/OperatorConfig.js +88 -0
  22. package/dist/OperatorConfig.js.map +1 -0
  23. package/dist/OperatorNode.d.ts +45 -0
  24. package/dist/OperatorNode.d.ts.map +1 -0
  25. package/dist/OperatorNode.js +103 -0
  26. package/dist/OperatorNode.js.map +1 -0
  27. package/dist/OperatorRuns.d.ts +76 -0
  28. package/dist/OperatorRuns.d.ts.map +1 -0
  29. package/dist/OperatorRuns.js +146 -0
  30. package/dist/OperatorRuns.js.map +1 -0
  31. package/dist/OperatorServer.d.ts +160 -0
  32. package/dist/OperatorServer.d.ts.map +1 -0
  33. package/dist/OperatorServer.js +123 -0
  34. package/dist/OperatorServer.js.map +1 -0
  35. package/dist/OperatorTools.d.ts +558 -0
  36. package/dist/OperatorTools.d.ts.map +1 -0
  37. package/dist/OperatorTools.js +398 -0
  38. package/dist/OperatorTools.js.map +1 -0
  39. package/dist/Refine.d.ts +2 -2
  40. package/dist/Refine.d.ts.map +1 -1
  41. package/dist/Refine.js +3 -5
  42. package/dist/Refine.js.map +1 -1
  43. package/dist/RunSupervisor.d.ts +51 -0
  44. package/dist/RunSupervisor.d.ts.map +1 -0
  45. package/dist/RunSupervisor.js +282 -0
  46. package/dist/RunSupervisor.js.map +1 -0
  47. package/dist/ShellErrors.d.ts +9 -0
  48. package/dist/ShellErrors.d.ts.map +1 -0
  49. package/dist/ShellErrors.js +7 -0
  50. package/dist/ShellErrors.js.map +1 -0
  51. package/flows/lib/epic-stories.js +83 -30
  52. package/flows/port-guide.js +4 -1
  53. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/pack.md +1 -0
  54. package/kits/j2ee-nextjs/packs/jsp-nextjs/pack.md +1 -0
  55. package/kits/port/packs/scala-ts/pack.md +1 -0
  56. package/kits/soap-ace/fixtures/demo-bank-soap/RUNBOOK.md +5 -2
  57. package/kits/soap-ace/flows/soap-epic.js +1 -2
  58. package/package.json +13 -4
  59. package/src/Cli.ts +64 -3
  60. package/src/FlowLaunch.ts +20 -66
  61. package/src/FlowProcess.ts +239 -0
  62. package/src/OperatorActTools.ts +407 -0
  63. package/src/OperatorConfig.ts +148 -0
  64. package/src/OperatorNode.ts +147 -0
  65. package/src/OperatorRuns.ts +270 -0
  66. package/src/OperatorServer.ts +192 -0
  67. package/src/OperatorTools.ts +610 -0
  68. package/src/Refine.ts +4 -7
  69. package/src/RunSupervisor.ts +403 -0
  70. 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 { ApprovedMarker, DraftApprovalMarker } from "@llm4ts/flow/Approval"
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()