@llm4ts/shell 2.17.0 → 2.18.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/flows/epic-design.js +33 -7
- package/flows/lib/epic-design.js +125 -14
- package/flows/lib/epic-stories.js +24 -0
- package/package.json +4 -4
package/flows/epic-design.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// llm4ts run epic-design --repo ~/work/portal "Current account: balance, movements, statements"
|
|
5
5
|
// llm4ts run epic-design --repo ~/work/portal -- --epic <id> # revise, or validate once approved
|
|
6
6
|
// llm4ts run epic-design --repo ~/work/portal -- --list # the repository's briefs
|
|
7
|
+
// llm4ts run epic-design --repo ~/work/portal -- --coverage # the ledger across every brief
|
|
7
8
|
//
|
|
8
9
|
// Runs rooted at the TARGET repository. The legacy repository must hold
|
|
9
10
|
// modernize-extract's pack (docs/modernization/); modernize-refine's
|
|
@@ -18,34 +19,57 @@
|
|
|
18
19
|
// from the approved brief. Seat: LLM4TS_REASONER (read-only, default
|
|
19
20
|
// claude). LLM4TS_PACK describes the target stack when the target is new.
|
|
20
21
|
// LLM4TS_CONTEXT_BUDGET bounds the legacy evidence in the prompt.
|
|
22
|
+
//
|
|
23
|
+
// Briefs of one repository are read together. What the approved briefs of
|
|
24
|
+
// other epics decided is inherited: a scenario they dropped or found in the
|
|
25
|
+
// target is not restated, one they own is not proposed again, one they
|
|
26
|
+
// deferred is offered; deciding otherwise is a `[check]` open point naming
|
|
27
|
+
// the other epic. --coverage writes .llm4ts/epics/coverage.md, the ledger of
|
|
28
|
+
// every scenario of the pack against every brief, without calling a model.
|
|
21
29
|
import { access, readdir } from "node:fs/promises";
|
|
22
|
-
import { join } from "node:path";
|
|
30
|
+
import { join, resolve } from "node:path";
|
|
23
31
|
import * as Console from "effect/Console";
|
|
24
32
|
import * as Effect from "effect/Effect";
|
|
25
33
|
import { budget, cap } from "@llm4ts/flow/Context";
|
|
26
34
|
import { Info, ScriptUsage, asReadOnly, nodePlainFileStore, openPack, resolveFlowInput, runFlowMain, runNode, stage } from "@llm4ts/runner";
|
|
27
|
-
import { designEpic, parseEpicDesignArgs, renderBriefList, renderOutcome, resolveDesignTarget } from "./lib/epic-design.js";
|
|
28
|
-
import { epicDirs, listBriefs, reasonerFromEnvironment } from "./lib/epic-stories.js";
|
|
35
|
+
import { coverageReport, designEpic, parseEpicDesignArgs, renderBriefList, renderOutcome, resolveDesignTarget } from "./lib/epic-design.js";
|
|
36
|
+
import { epicDirs, listBriefs, listEpics, loadBriefs, reasonerFromEnvironment } from "./lib/epic-stories.js";
|
|
29
37
|
import { ModDir } from "./lib/modernize-extract.js";
|
|
30
38
|
const program = Effect.gen(function* () {
|
|
31
39
|
const flags = yield* parseEpicDesignArgs(process.argv.slice(2));
|
|
32
40
|
const given = yield* resolveFlowInput("", flags.rest);
|
|
33
41
|
const files = nodePlainFileStore;
|
|
34
|
-
const
|
|
42
|
+
const dirs = yield* epicDirs(given.workDir);
|
|
43
|
+
const briefs = yield* listBriefs(files, given.workDir, dirs);
|
|
35
44
|
if (flags.list) {
|
|
36
45
|
yield* Console.log(renderBriefList(briefs));
|
|
37
46
|
return;
|
|
38
47
|
}
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
48
|
+
const legacyGiven = process.env.LLM4TS_LEGACY_REPO?.trim();
|
|
49
|
+
if (legacyGiven === undefined || legacyGiven.length === 0) {
|
|
41
50
|
return yield* ScriptUsage.make({
|
|
42
51
|
message: "LLM4TS_LEGACY_REPO is not set: point it at the legacy repository holding docs/modernization/"
|
|
43
52
|
});
|
|
44
53
|
}
|
|
45
|
-
|
|
54
|
+
// One spelling of the path, so every brief of this repository names the same legacy.
|
|
55
|
+
const legacyRepo = resolve(given.workspace, legacyGiven);
|
|
46
56
|
const specNames = yield* Effect.tryPromise(() => readdir(join(legacyRepo, ModDir, "specs"))).pipe(Effect.map((names) => names
|
|
47
57
|
.filter((name) => name.endsWith(".md") && name !== "README.md")
|
|
48
58
|
.map((name) => name.slice(0, -".md".length))), Effect.catch(() => Effect.succeed([])));
|
|
59
|
+
if (flags.coverage) {
|
|
60
|
+
const report = yield* coverageReport({
|
|
61
|
+
files,
|
|
62
|
+
targetDir: given.workDir,
|
|
63
|
+
legacyRepo,
|
|
64
|
+
specNames,
|
|
65
|
+
dirs,
|
|
66
|
+
epics: yield* listEpics(files, given.workDir)
|
|
67
|
+
});
|
|
68
|
+
yield* Console.log(`${report.headline}\n${report.path}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const target = yield* resolveDesignTarget({ text: given.prompt, epic: flags.epic, briefs });
|
|
72
|
+
const loaded = yield* loadBriefs(files, given.workDir, dirs);
|
|
49
73
|
const reasoning = asReadOnly(yield* reasonerFromEnvironment(process.env));
|
|
50
74
|
yield* runNode({
|
|
51
75
|
workDir: given.workDir,
|
|
@@ -83,6 +107,8 @@ const program = Effect.gen(function* () {
|
|
|
83
107
|
budget: budget(process.env),
|
|
84
108
|
guidance,
|
|
85
109
|
packNote,
|
|
110
|
+
others: loaded.briefs,
|
|
111
|
+
unreadable: loaded.unreadable,
|
|
86
112
|
pathExists: (absolute) => Effect.tryPromise(() => access(absolute)).pipe(Effect.as(true), Effect.catch(() => Effect.succeed(false)))
|
|
87
113
|
}));
|
|
88
114
|
for (const line of renderOutcome(outcome, target.epicId)) {
|
package/flows/lib/epic-design.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import { cap } from "@llm4ts/flow/Context";
|
|
8
|
+
import { buildLedger, inheritedFrom, ledgerHeadline, renderLedger } from "@llm4ts/flow/CoverageLedger";
|
|
8
9
|
import { parseDecisions, scenarioTitles } from "@llm4ts/flow/Decisions";
|
|
9
|
-
import { assembleBrief, checkEpicBrief, diffBriefs, EpicBriefProposal, epicBriefProposalJsonSchema, loopAction, normalizeBrief, parseEpicBrief, ProgramSelection, programSelectionJsonSchema, providedPointers, renderBriefProblem, renderEpicBrief, unanswered } from "@llm4ts/flow/EpicBrief";
|
|
10
|
+
import { assembleBrief, checkEpicBrief, diffBriefs, dispositionWords, EpicBriefProposal, epicBriefProposalJsonSchema, loopAction, normalizeBrief, parseEpicBrief, ProgramSelection, programSelectionJsonSchema, providedPointers, renderBriefProblem, renderEpicBrief, unanswered } from "@llm4ts/flow/EpicBrief";
|
|
10
11
|
import { structuredAndPublish } from "@llm4ts/flow/Flow";
|
|
11
12
|
import { EpicBriefInvalid, ExtractPackMissing, OpenPointsPending } from "@llm4ts/flow/FlowError";
|
|
12
13
|
import { ScriptUsage } from "@llm4ts/runner";
|
|
13
|
-
import { epicIdFor, epicsDir } from "./epic-stories.js";
|
|
14
|
+
import { epicIdFor, epicsDir, loadBriefs } from "./epic-stories.js";
|
|
14
15
|
import { ModDir } from "./modernize-extract.js";
|
|
15
16
|
// ---- Arguments ----------------------------------------------------------------
|
|
16
17
|
export const epicDesignUsage = [
|
|
@@ -18,10 +19,12 @@ export const epicDesignUsage = [
|
|
|
18
19
|
" LLM4TS_LEGACY_REPO=<path> the legacy repository holding docs/modernization/ (required)",
|
|
19
20
|
" LLM4TS_PACK=<name|dir> the pack describing the target stack (a new or empty target)",
|
|
20
21
|
" --epic <id> revise or validate an existing brief, no text needed",
|
|
21
|
-
" --list this repository's briefs and their status"
|
|
22
|
+
" --list this repository's briefs and their status",
|
|
23
|
+
" --coverage write the coverage ledger across every brief (no model call)"
|
|
22
24
|
].join("\n");
|
|
23
25
|
export const parseEpicDesignArgs = (argv) => Effect.gen(function* () {
|
|
24
26
|
let list = false;
|
|
27
|
+
let coverage = false;
|
|
25
28
|
let epic;
|
|
26
29
|
const rest = [];
|
|
27
30
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -29,6 +32,9 @@ export const parseEpicDesignArgs = (argv) => Effect.gen(function* () {
|
|
|
29
32
|
if (argument === "--list") {
|
|
30
33
|
list = true;
|
|
31
34
|
}
|
|
35
|
+
else if (argument === "--coverage") {
|
|
36
|
+
coverage = true;
|
|
37
|
+
}
|
|
32
38
|
else if (argument === "--epic" || argument.startsWith("--epic=")) {
|
|
33
39
|
const value = argument.includes("=") ? argument.slice("--epic=".length) : argv[index + 1];
|
|
34
40
|
if (!argument.includes("="))
|
|
@@ -50,7 +56,7 @@ export const parseEpicDesignArgs = (argv) => Effect.gen(function* () {
|
|
|
50
56
|
rest.push(argument);
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
|
-
return { list, epic, rest };
|
|
59
|
+
return { list, coverage, epic, rest };
|
|
54
60
|
});
|
|
55
61
|
// ---- Which brief ---------------------------------------------------------------
|
|
56
62
|
export const renderBriefList = (briefs) => briefs.length === 0
|
|
@@ -156,6 +162,16 @@ export const selectPrompt = (request, index) => [
|
|
|
156
162
|
].join("\n");
|
|
157
163
|
export const proposePrompt = (options) => {
|
|
158
164
|
const perProgram = Math.max(2_000, Math.floor(options.budget / Math.max(1, options.programs.length)));
|
|
165
|
+
const selected = new Set(options.programs.map((program) => program.name));
|
|
166
|
+
const inherited = (options.inherited ?? []).filter((decision) => selected.has(decision.program));
|
|
167
|
+
const decided = inherited.filter((decision) => decision.disposition !== "deferred");
|
|
168
|
+
const available = inherited.filter((decision) => decision.disposition === "deferred");
|
|
169
|
+
const settled = new Set(decided.map((decision) => `${decision.program}\u0000${decision.scenario}`));
|
|
170
|
+
const open = options.pack.index.programs
|
|
171
|
+
.filter((program) => selected.has(program.name))
|
|
172
|
+
.flatMap((program) => program.scenarios
|
|
173
|
+
.filter((scenario) => !settled.has(`${program.name}\u0000${scenario}`))
|
|
174
|
+
.map((scenario) => `- ${program.name} › ${scenario}`));
|
|
159
175
|
const evidence = options.programs.flatMap((program) => [
|
|
160
176
|
`===== ${program.name} — spec =====`,
|
|
161
177
|
cap(options.pack.specs[program.name] ?? "", perProgram).text,
|
|
@@ -206,11 +222,27 @@ export const proposePrompt = (options) => {
|
|
|
206
222
|
options.guidance,
|
|
207
223
|
...(options.packNote === undefined ? [] : ["", "The target stack's pack:", options.packNote]),
|
|
208
224
|
"",
|
|
225
|
+
...(decided.length === 0
|
|
226
|
+
? []
|
|
227
|
+
: [
|
|
228
|
+
"Already decided by other epics (do not restate; they are inherited):",
|
|
229
|
+
...decided.map((decision) => `- ${decision.program} › ${decision.scenario} — ${decision.disposition === "dropped"
|
|
230
|
+
? `dropped by epic ${decision.epic}`
|
|
231
|
+
: `${dispositionWords[decision.disposition]} in epic ${decision.epic}`}${decision.note.length === 0 ? "" : ` (${decision.note})`}`),
|
|
232
|
+
""
|
|
233
|
+
]),
|
|
234
|
+
...(available.length === 0
|
|
235
|
+
? []
|
|
236
|
+
: [
|
|
237
|
+
"Deferred by other epics, available to this one (claim them if they belong here):",
|
|
238
|
+
...available.map((decision) => `- ${decision.program} › ${decision.scenario} — deferred by epic ${decision.epic}${decision.note.length === 0 ? "" : `: ${decision.note}`}`),
|
|
239
|
+
""
|
|
240
|
+
]),
|
|
209
241
|
"Every scenario to give a disposition to (the complete list; the evidence below may be",
|
|
210
242
|
"abridged, these titles are not):",
|
|
211
|
-
...
|
|
212
|
-
|
|
213
|
-
|
|
243
|
+
...(open.length === 0
|
|
244
|
+
? ["(none: every scenario of these programs is already decided by other epics)"]
|
|
245
|
+
: open),
|
|
214
246
|
"",
|
|
215
247
|
"Legacy evidence:",
|
|
216
248
|
...evidence
|
|
@@ -233,14 +265,53 @@ const verifiedPointers = (deps, brief) => Effect.gen(function* () {
|
|
|
233
265
|
}
|
|
234
266
|
return found;
|
|
235
267
|
});
|
|
236
|
-
const check = (deps, pack, brief) => Effect.map(verifiedPointers(deps, brief), (pointers) => checkEpicBrief(brief, { pack: pack.index, pointers }));
|
|
268
|
+
const check = (deps, pack, brief, others) => Effect.map(verifiedPointers(deps, brief), (pointers) => checkEpicBrief(brief, { pack: pack.index, pointers, others, epicId: deps.epicId }));
|
|
269
|
+
/** The ledger of the other epics' briefs, against this pack. */
|
|
270
|
+
const ledgerOfOthers = (deps, pack) => buildLedger({
|
|
271
|
+
pack: pack.index,
|
|
272
|
+
briefs: (deps.others ?? []).filter((other) => other.epicId !== deps.epicId),
|
|
273
|
+
epics: [],
|
|
274
|
+
legacy: deps.legacyRepo
|
|
275
|
+
});
|
|
276
|
+
/** Other briefs that fed nothing into this run, said in words. */
|
|
277
|
+
const unusedBriefs = (deps, others) => [
|
|
278
|
+
...(deps.unreadable ?? [])
|
|
279
|
+
.filter((brief) => brief.dir !== deps.epicId)
|
|
280
|
+
.map((brief) => `brief '${brief.dir}' could not be read (${brief.reason}): nothing is inherited from it`),
|
|
281
|
+
...others.skipped.map((epic) => `brief '${epic}' was designed against another legacy repository: nothing is inherited from it`)
|
|
282
|
+
];
|
|
283
|
+
/** Per epic, how many scenarios of the considered programs a brief inherits. */
|
|
284
|
+
const inheritedSummary = (inherited, programs) => {
|
|
285
|
+
const considered = new Set(programs.map((program) => program.name));
|
|
286
|
+
const perEpic = new Map();
|
|
287
|
+
for (const decision of inherited) {
|
|
288
|
+
if (decision.disposition === "deferred" || !considered.has(decision.program))
|
|
289
|
+
continue;
|
|
290
|
+
const seen = perEpic.get(decision.epic) ?? new Set();
|
|
291
|
+
seen.add(`${decision.program}\u0000${decision.scenario}`);
|
|
292
|
+
perEpic.set(decision.epic, seen);
|
|
293
|
+
}
|
|
294
|
+
return [...perEpic].map(([epic, scenarios]) => ({ epic, scenarios: scenarios.size }));
|
|
295
|
+
};
|
|
237
296
|
export const designEpic = Effect.fn("epic-design.design")(function* (deps) {
|
|
238
297
|
const path = briefPath(deps.targetDir, deps.epicId);
|
|
239
298
|
const pack = yield* readPackIndex(deps);
|
|
240
299
|
const onDisk = yield* deps.files.read(path);
|
|
241
300
|
const current = onDisk === undefined ? undefined : yield* parseEpicBrief(onDisk, path);
|
|
242
301
|
const action = loopAction(current);
|
|
243
|
-
const
|
|
302
|
+
const others = ledgerOfOthers(deps, pack);
|
|
303
|
+
const inherited = inheritedFrom(others);
|
|
304
|
+
const warnings = unusedBriefs(deps, others);
|
|
305
|
+
const outcome = (changes, problems, openPoints, considered = []) => ({
|
|
306
|
+
action,
|
|
307
|
+
path,
|
|
308
|
+
targetDir: deps.targetDir,
|
|
309
|
+
changes,
|
|
310
|
+
problems,
|
|
311
|
+
openPoints,
|
|
312
|
+
inherited: inheritedSummary(inherited, considered),
|
|
313
|
+
warnings
|
|
314
|
+
});
|
|
244
315
|
if (current !== undefined && action === "halt") {
|
|
245
316
|
return yield* OpenPointsPending.make({
|
|
246
317
|
path,
|
|
@@ -251,7 +322,7 @@ export const designEpic = Effect.fn("epic-design.design")(function* (deps) {
|
|
|
251
322
|
return outcome([], [], 0);
|
|
252
323
|
}
|
|
253
324
|
if (current !== undefined && action === "validate") {
|
|
254
|
-
const problems = yield* check(deps, pack, current);
|
|
325
|
+
const problems = yield* check(deps, pack, current, inherited);
|
|
255
326
|
if (problems.length > 0) {
|
|
256
327
|
return yield* EpicBriefInvalid.make({ path, violations: problems.map(renderBriefProblem) });
|
|
257
328
|
}
|
|
@@ -270,6 +341,7 @@ export const designEpic = Effect.fn("epic-design.design")(function* (deps) {
|
|
|
270
341
|
budget: deps.budget,
|
|
271
342
|
guidance: deps.guidance,
|
|
272
343
|
packNote: deps.packNote,
|
|
344
|
+
inherited,
|
|
273
345
|
...(current === undefined ? {} : { current }),
|
|
274
346
|
...(problems === undefined ? {} : { problems })
|
|
275
347
|
}), EpicBriefProposal, epicBriefProposalJsonSchema);
|
|
@@ -290,11 +362,11 @@ export const designEpic = Effect.fn("epic-design.design")(function* (deps) {
|
|
|
290
362
|
? proposed
|
|
291
363
|
: EpicBriefProposal.make({ ...proposed, openPoints: [noPrograms, ...proposed.openPoints] });
|
|
292
364
|
let proposal = withNotice(yield* propose());
|
|
293
|
-
let problems = yield* check(deps, pack, assemble(proposal));
|
|
365
|
+
let problems = yield* check(deps, pack, assemble(proposal), inherited);
|
|
294
366
|
if (problems.length > 0) {
|
|
295
367
|
// One bounded fix round; what still fails is shown to the human, never hidden.
|
|
296
368
|
proposal = withNotice(yield* propose(problems));
|
|
297
|
-
problems = yield* check(deps, pack, assemble(proposal));
|
|
369
|
+
problems = yield* check(deps, pack, assemble(proposal), inherited);
|
|
298
370
|
}
|
|
299
371
|
const next = assemble(proposal, problems);
|
|
300
372
|
// What is written must read back as the same brief: a file that parses to
|
|
@@ -309,10 +381,10 @@ export const designEpic = Effect.fn("epic-design.design")(function* (deps) {
|
|
|
309
381
|
});
|
|
310
382
|
}
|
|
311
383
|
yield* deps.files.writeAtomic(path, written);
|
|
312
|
-
return outcome(diffBriefs(current, next), problems, next.openPoints.length);
|
|
384
|
+
return outcome(diffBriefs(current, next), problems, next.openPoints.length, next.programs);
|
|
313
385
|
});
|
|
314
386
|
/** What the flow prints after a run. */
|
|
315
|
-
|
|
387
|
+
const outcomeLines = (outcome, epicId) => {
|
|
316
388
|
const approve = `set \`Status: approved\` in ${outcome.path} once it is right`;
|
|
317
389
|
const plan = `llm4ts run epic-stories --repo ${outcome.targetDir} -- --epic ${epicId}`;
|
|
318
390
|
switch (outcome.action) {
|
|
@@ -330,6 +402,13 @@ export const renderOutcome = (outcome, epicId) => {
|
|
|
330
402
|
return [
|
|
331
403
|
`${outcome.action === "propose" ? "proposed" : "revised"} ${outcome.path}:`,
|
|
332
404
|
...outcome.changes.map((change) => ` ${change}`),
|
|
405
|
+
...(outcome.inherited.length === 0
|
|
406
|
+
? []
|
|
407
|
+
: [
|
|
408
|
+
` inherited from ${outcome.inherited
|
|
409
|
+
.map((entry) => `${entry.epic} (${entry.scenarios} scenario${entry.scenarios === 1 ? "" : "s"})`)
|
|
410
|
+
.join(", ")}: not restated here; see \`epic-design --coverage\``
|
|
411
|
+
]),
|
|
333
412
|
...(outcome.problems.length === 0
|
|
334
413
|
? []
|
|
335
414
|
: [` ${outcome.problems.length} check(s) still failing, raised as [check] open points`]),
|
|
@@ -339,3 +418,35 @@ export const renderOutcome = (outcome, epicId) => {
|
|
|
339
418
|
];
|
|
340
419
|
}
|
|
341
420
|
};
|
|
421
|
+
/** What the flow prints after a run; briefs it could not use are always said. */
|
|
422
|
+
export const renderOutcome = (outcome, epicId) => [
|
|
423
|
+
...outcomeLines(outcome, epicId),
|
|
424
|
+
...outcome.warnings.map((warning) => `warning: ${warning}`)
|
|
425
|
+
];
|
|
426
|
+
// ---- The coverage ledger ---------------------------------------------------------
|
|
427
|
+
export const coveragePath = (targetDir) => join(epicsDir(targetDir), "coverage.md");
|
|
428
|
+
/** What epic-stories recorded for each planned epic, as the ledger reads it. */
|
|
429
|
+
export const epicProgressOf = (epics) => epics.map((epic) => ({
|
|
430
|
+
epicId: epic.dir,
|
|
431
|
+
planned: true,
|
|
432
|
+
stories: epic.stories,
|
|
433
|
+
merged: epic.merged,
|
|
434
|
+
landed: epic.landed !== undefined
|
|
435
|
+
}));
|
|
436
|
+
/**
|
|
437
|
+
* The ledger across every brief of the target, written as coverage.md. A
|
|
438
|
+
* read of the repository: no model is called and no brief is edited.
|
|
439
|
+
*/
|
|
440
|
+
export const coverageReport = Effect.fn("epic-design.coverage")(function* (options) {
|
|
441
|
+
const pack = yield* readPackIndex(options);
|
|
442
|
+
const loaded = yield* loadBriefs(options.files, options.targetDir, options.dirs);
|
|
443
|
+
const ledger = buildLedger({
|
|
444
|
+
pack: pack.index,
|
|
445
|
+
briefs: loaded.briefs,
|
|
446
|
+
epics: epicProgressOf(options.epics),
|
|
447
|
+
legacy: options.legacyRepo
|
|
448
|
+
});
|
|
449
|
+
const path = coveragePath(options.targetDir);
|
|
450
|
+
yield* options.files.writeAtomic(path, renderLedger(ledger, { legacy: options.legacyRepo, unreadable: loaded.unreadable }));
|
|
451
|
+
return { path, headline: ledgerHeadline(ledger), ledger, unreadable: loaded.unreadable };
|
|
452
|
+
});
|
|
@@ -5,6 +5,7 @@ import { readdir } from "node:fs/promises";
|
|
|
5
5
|
import { isAbsolute, join, normalize } from "node:path";
|
|
6
6
|
import * as Duration from "effect/Duration";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Result from "effect/Result";
|
|
8
9
|
import * as Schema from "effect/Schema";
|
|
9
10
|
import { Dimension, Sample } from "@llm4ts/core/eval/Eval";
|
|
10
11
|
import { judge } from "@llm4ts/core/eval/Judge";
|
|
@@ -187,6 +188,29 @@ export const listBriefs = (files, workDir, dirs) => Effect.gen(function* () {
|
|
|
187
188
|
}
|
|
188
189
|
return briefs;
|
|
189
190
|
});
|
|
191
|
+
/**
|
|
192
|
+
* Every brief of the repository, parsed, for the coverage ledger. A folder
|
|
193
|
+
* without a brief is skipped; a brief that does not parse is reported with
|
|
194
|
+
* the first thing wrong in it, never dropped silently.
|
|
195
|
+
*/
|
|
196
|
+
export const loadBriefs = (files, workDir, dirs) => Effect.gen(function* () {
|
|
197
|
+
const briefs = [];
|
|
198
|
+
const unreadable = [];
|
|
199
|
+
for (const dir of dirs) {
|
|
200
|
+
const path = join(epicsDir(workDir), dir, "brief.md");
|
|
201
|
+
const text = yield* files.read(path);
|
|
202
|
+
if (text === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
const parsed = yield* Effect.result(parseEpicBrief(text, path));
|
|
205
|
+
if (Result.isSuccess(parsed)) {
|
|
206
|
+
briefs.push({ epicId: dir, brief: parsed.success });
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
unreadable.push({ dir, reason: parsed.failure.violations[0] ?? "does not parse" });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { briefs, unreadable };
|
|
213
|
+
});
|
|
190
214
|
/**
|
|
191
215
|
* What the story planner reads in place of the one-line epic: the approved
|
|
192
216
|
* brief in the epic's folder. Undefined when the plan is already written (the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/shell",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
54
54
|
"@effect/platform-node-shared": "4.0.0-rc.115",
|
|
55
|
-
"@llm4ts/core": "2.
|
|
56
|
-
"@llm4ts/
|
|
57
|
-
"@llm4ts/
|
|
55
|
+
"@llm4ts/core": "2.18.0",
|
|
56
|
+
"@llm4ts/runner": "2.18.0",
|
|
57
|
+
"@llm4ts/flow": "2.18.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"effect": "4.0.0-rc.115"
|