@llm4ts/shell 2.30.0 → 2.31.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.
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Epic: epic-light
|
|
2
|
+
|
|
3
|
+
Add a shared format contract, greet in English or Italian, and keep a click history in the counter; restore the skipped counter test on the way.
|
|
4
|
+
|
|
5
|
+
## Waves
|
|
6
|
+
|
|
7
|
+
1. format-contract
|
|
8
|
+
2. greeting-locale, counter-history
|
|
9
|
+
|
|
10
|
+
## Stories
|
|
11
|
+
|
|
12
|
+
The fixed plan of the light comparison epic against the `epic-light` starter (`examples/starters/epic-light`, seeded by `examples/seed.sh epic`). The block below is the source of truth: the seed drops it at `.llm4ts/epics/epic-light/plan.md`, so a run plans nothing and two releases compare on the same stories. Planted on purpose: `src/platform/clock.test.ts` is red on `main` and no story owns it; `counter.test.ts` skips one test that `counter-history` must restore; `counter-history` cannot meet its acceptance without changing an existing expectation and declares `testsChange: true`.
|
|
13
|
+
|
|
14
|
+
## Plan block
|
|
15
|
+
|
|
16
|
+
```json storyplan
|
|
17
|
+
{
|
|
18
|
+
"epicId": "epic-light",
|
|
19
|
+
"epic": "Add a shared format contract, greet in English or Italian, and keep a click history in the counter; restore the skipped counter test on the way.",
|
|
20
|
+
"stories": [
|
|
21
|
+
{
|
|
22
|
+
"id": "format-contract",
|
|
23
|
+
"title": "Shared format contract",
|
|
24
|
+
"description": "Create src/contracts/format.ts exporting a `Locale` type (\"en\" | \"it\"), `formatCount(count: number, locale: Locale): string` (\"3 clicks\" / \"3 clic\", singular \"1 click\" / \"1 clic\"), and `formatName(name: string): string` (trimmed, first letter upper-cased). Add src/contracts/format.test.ts covering both locales, the singular, and the name trimming. Follow CONTRIBUTING.md: one describe per module, Vitest imports.",
|
|
25
|
+
"dependsOn": [],
|
|
26
|
+
"owned": ["src/contracts/format.ts", "src/contracts/format.test.ts"],
|
|
27
|
+
"sharedReadOnly": ["CONTRIBUTING.md", "src/features/greeting/greeting.ts"],
|
|
28
|
+
"readFirst": ["src/features/greeting/greeting.ts", "CONTRIBUTING.md"],
|
|
29
|
+
"acceptance": [
|
|
30
|
+
"src/contracts/format.ts exports Locale, formatCount and formatName",
|
|
31
|
+
"formatCount(1, \"en\") is \"1 click\" and formatCount(3, \"it\") is \"3 clic\"",
|
|
32
|
+
"src/contracts/format.test.ts exists and covers both locales and formatName"
|
|
33
|
+
],
|
|
34
|
+
"provides": ["Locale, formatCount, formatName from src/contracts/format.ts"]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "greeting-locale",
|
|
38
|
+
"title": "Greeting in English or Italian",
|
|
39
|
+
"description": "Extend src/features/greeting/greeting.ts so `greet(name, locale)` takes a `Locale` from src/contracts/format.ts and answers \"Hello, <Name>!\" in English or \"Ciao, <Name>!\" in Italian, passing the name through `formatName`. Keep the existing one-argument call working (English by default). Extend greeting.test.ts with the Italian case and the default.",
|
|
40
|
+
"dependsOn": ["format-contract"],
|
|
41
|
+
"owned": ["src/features/greeting"],
|
|
42
|
+
"sharedReadOnly": ["src/contracts", "CONTRIBUTING.md"],
|
|
43
|
+
"readFirst": ["src/contracts/format.ts", "src/features/greeting/greeting.ts"],
|
|
44
|
+
"acceptance": [
|
|
45
|
+
"greet(\"ada\", \"it\") is \"Ciao, Ada!\" and greet(\"Ada\") is still \"Hello, Ada!\"",
|
|
46
|
+
"greeting.test.ts covers the Italian greeting and the default locale",
|
|
47
|
+
"nothing outside src/features/greeting changes"
|
|
48
|
+
],
|
|
49
|
+
"provides": ["greet(name, locale?) from src/features/greeting/greeting.ts"]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "counter-history",
|
|
53
|
+
"title": "Counter with click history",
|
|
54
|
+
"description": "Give the counter in src/features/counter/counter.ts a `history: ReadonlyArray<number>` of every count it has had (make() starts with [0]; increment and reset append). Un-skip the \"resets to zero\" test in counter.test.ts and make it pass; update the \"starts at zero\" expectation to the new shape and add a test for the history. Then update src/app.ts (the composition point this story owns) so `run(name, locale)` greets through greet(name, locale) and renders the count with formatCount from src/contracts/format.ts.",
|
|
55
|
+
"dependsOn": ["format-contract"],
|
|
56
|
+
"owned": ["src/features/counter", "src/app.ts"],
|
|
57
|
+
"sharedReadOnly": ["src/contracts", "src/features/greeting", "CONTRIBUTING.md"],
|
|
58
|
+
"readFirst": ["src/features/counter/counter.ts", "src/contracts/format.ts", "src/app.ts"],
|
|
59
|
+
"acceptance": [
|
|
60
|
+
"make().history is [0] and increment(make()).history is [0, 1]",
|
|
61
|
+
"counter.test.ts has no it.skip and its reset test passes",
|
|
62
|
+
"run(\"ada\", \"it\") contains \"Ciao, Ada!\" and \"1 clic\""
|
|
63
|
+
],
|
|
64
|
+
"provides": ["Counter.history from src/features/counter/counter.ts", "run(name, locale) from src/app.ts"],
|
|
65
|
+
"testsChange": true
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
```
|
package/flows/implement.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Persistent plan: plan the task, then implement, review, and commit one task at a time.
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import { withContract } from "@llm4ts/flow/AutonomyContract";
|
|
4
5
|
import { coderFromEnv, defaultPlanPath, implementPlanFlow, makePlanStore, nodePlainFileStore, planFrom, resolveFlowInput, runFlowMain, runNode } from "@llm4ts/runner";
|
|
5
6
|
const program = Effect.gen(function* () {
|
|
6
7
|
const input = yield* resolveFlowInput("Add a multiply function to the calculator, including focused tests.");
|
|
@@ -16,7 +17,7 @@ const program = Effect.gen(function* () {
|
|
|
16
17
|
store,
|
|
17
18
|
planPath,
|
|
18
19
|
plan: planFrom(context.reasoning, input.prompt),
|
|
19
|
-
system: "Implement one task at a time in the current repository."
|
|
20
|
+
system: withContract("Implement one task at a time in the current repository.")
|
|
20
21
|
}));
|
|
21
22
|
});
|
|
22
23
|
runFlowMain(program);
|
package/flows/issue-pr.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// GitHub issue to pull request: assess the issue, plan, implement, push, and open a PR.
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import { withContract } from "@llm4ts/flow/AutonomyContract";
|
|
4
5
|
import { FlowAborted, ScriptUsage, coderFromEnv, implementPlanFlow, makePlanStore, nodePlainFileStore, resolveFlowInput, runFlowMain, runNode, stage } from "@llm4ts/runner";
|
|
5
6
|
import { parseIssueRef } from "@llm4ts/flow/GitHubTool";
|
|
6
7
|
import { assessThenPlan } from "@llm4ts/flow/Planner";
|
|
@@ -44,7 +45,7 @@ const program = Effect.gen(function* () {
|
|
|
44
45
|
store,
|
|
45
46
|
planPath,
|
|
46
47
|
plan: Effect.succeed(plan),
|
|
47
|
-
system: "Implement one issue task at a time in the current repository.",
|
|
48
|
+
system: withContract("Implement one issue task at a time in the current repository."),
|
|
48
49
|
reviewers: allReviewers
|
|
49
50
|
});
|
|
50
51
|
yield* stage(context.events, "push branch", context.git.push("origin", completed.epicId));
|
|
@@ -26,6 +26,8 @@ import { loadVersioned } from "@llm4ts/flow/Persistence";
|
|
|
26
26
|
import { BlockedVerdict, StoryState, StoryStateVersion, StoryVerdict } from "@llm4ts/flow/Stories";
|
|
27
27
|
import { stableHash } from "@llm4ts/flow/Plan";
|
|
28
28
|
import { ReviewIssue } from "@llm4ts/flow/Review";
|
|
29
|
+
import { compactGateArtifacts, gatesIn } from "@llm4ts/flow/Gates";
|
|
30
|
+
import { loadTranscript, nodeTranscriptFiles } from "@llm4ts/runner/Transcripts";
|
|
29
31
|
import { StoryPlan, dependenciesOf, makeStoryPlanStore, parseStoryPlan, isSafeAnchor, pathsNamedIn, pruneReadFirst } from "@llm4ts/flow/StoryPlan";
|
|
30
32
|
import { defaultOrientationChars, orientationChars, orientationOf } from "@llm4ts/flow/Orientation";
|
|
31
33
|
import * as Clock from "effect/Clock";
|
|
@@ -39,7 +41,7 @@ import { implementStoriesFlow } from "@llm4ts/flow/Stories";
|
|
|
39
41
|
import { validateStoryPlan } from "@llm4ts/flow/StoryPlan";
|
|
40
42
|
import { asReadOnly, nodePlainFileStore, nodeProcessExecutor, resolveFlowInput, runNode, stage, withModel } from "@llm4ts/runner";
|
|
41
43
|
import { claude, coderIds, judgmentConnectorFromEnvironment, pi } from "@llm4ts/runner/Connectors";
|
|
42
|
-
import { FlowAborted, FlowLlmError, ReviewResult, ScriptUsage, coderFor, lintCommand
|
|
44
|
+
import { FlowAborted, FlowLlmError, ReviewResult, ScriptUsage, coderFor, lintCommand } from "@llm4ts/runner";
|
|
43
45
|
export const epicUsage = [
|
|
44
46
|
"epic-stories flags:",
|
|
45
47
|
" --plan-only write (or re-validate) the story plan and stop",
|
|
@@ -620,7 +622,8 @@ export const storyPlanJsonSchema = {
|
|
|
620
622
|
sharedReadOnly: { type: "array", items: { type: "string" } },
|
|
621
623
|
readFirst: { type: "array", items: { type: "string" } },
|
|
622
624
|
acceptance: { type: "array", items: { type: "string" } },
|
|
623
|
-
provides: { type: "array", items: { type: "string" } }
|
|
625
|
+
provides: { type: "array", items: { type: "string" } },
|
|
626
|
+
testsChange: { type: "boolean" }
|
|
624
627
|
},
|
|
625
628
|
required: [
|
|
626
629
|
"id",
|
|
@@ -659,6 +662,9 @@ orientation) => [
|
|
|
659
662
|
" the diff by a reviewer who will not run the app (a route that answers, a screen that shows",
|
|
660
663
|
" X, a test file beside the feature that covers Y). The coder plans its tasks against them",
|
|
661
664
|
" and the judge scores against them.",
|
|
665
|
+
"- `testsChange`: true only for a story whose job includes deleting, skipping or rewriting",
|
|
666
|
+
" existing tests (a migration of a test suite, a removed feature). Every other story leaves",
|
|
667
|
+
" it out: the gates fail a story that deletes a test file or adds a skip marker.",
|
|
662
668
|
"- `owned` sets are pairwise DISJOINT: no path prefix appears under two stories.",
|
|
663
669
|
"- Shared surfaces (the kit, the theme, house rules) are never edited by a feature story. A new",
|
|
664
670
|
" shared component is its own story, and every story using it depends on it.",
|
|
@@ -969,7 +975,7 @@ export const appDirFor = (workDir, environment) => Effect.gen(function* () {
|
|
|
969
975
|
return candidates.length === 1 ? (candidates[0] ?? ".") : ".";
|
|
970
976
|
});
|
|
971
977
|
/** Runs a per-checkout step in the app directory of whichever checkout it is given. */
|
|
972
|
-
export const inAppDir = (appDir, step) => (workDir) => step(appDir === "." ? workDir : join(workDir, appDir));
|
|
978
|
+
export const inAppDir = (appDir, step) => (workDir, ...rest) => step(appDir === "." ? workDir : join(workDir, appDir), ...rest);
|
|
973
979
|
// ---- Worktree setup --------------------------------------------------------------
|
|
974
980
|
export const defaultWorktreeSetup = ["pnpm", "install", "--offline"];
|
|
975
981
|
/**
|
|
@@ -1041,18 +1047,27 @@ export const gateCommands = (environment, scripts) => {
|
|
|
1041
1047
|
.filter((part) => part.length > 0))
|
|
1042
1048
|
.filter((command) => command.length > 0);
|
|
1043
1049
|
};
|
|
1044
|
-
/**
|
|
1045
|
-
export
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
return
|
|
1055
|
-
}
|
|
1050
|
+
/** The gate runner lives in `@llm4ts/flow/Gates` (ADR 0027); re-exported for forks and tests. */
|
|
1051
|
+
export { gatesIn };
|
|
1052
|
+
/** `LLM4TS_GATE_TIMEOUT` in seconds (default 1200): a gate still running after it is a hang. */
|
|
1053
|
+
export const gateTimeoutSeconds = (environment) => {
|
|
1054
|
+
const raw = Number.parseInt(environment.LLM4TS_GATE_TIMEOUT ?? "", 10);
|
|
1055
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 1200;
|
|
1056
|
+
};
|
|
1057
|
+
/** `LLM4TS_GATE_TAIL_CHARS` (default 4000): how much gate output the fix prompt carries. */
|
|
1058
|
+
export const gateTailChars = (environment) => {
|
|
1059
|
+
const raw = Number.parseInt(environment.LLM4TS_GATE_TAIL_CHARS ?? "", 10);
|
|
1060
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 4000;
|
|
1061
|
+
};
|
|
1062
|
+
export const gateRunOptions = (environment) => ({ timeout: Duration.seconds(gateTimeoutSeconds(environment)) });
|
|
1063
|
+
/** The gate command that runs the tests, for the one flaky rerun; undefined when none does. */
|
|
1064
|
+
export const testGateCommand = (commands) => commands.find((command) => command.some((part, index) => index > 0 && /(?:^|[^a-z])(?:test|vitest|jest)/iu.test(part)));
|
|
1065
|
+
/**
|
|
1066
|
+
* How gate output reaches the coder: a capped tail, and the log's path. The
|
|
1067
|
+
* epic-stories coders are CLI harnesses that can open a file; an API coder
|
|
1068
|
+
* still gets the tail and ignores the path.
|
|
1069
|
+
*/
|
|
1070
|
+
export const fixPromptOptions = (environment) => ({ tailChars: gateTailChars(environment), showPaths: true });
|
|
1056
1071
|
/**
|
|
1057
1072
|
* Transcripts are on by default for epic-stories: a slow story is only
|
|
1058
1073
|
* explainable from them. `LLM4TS_TRANSCRIPT=off` (or `0`, `false`, `no`)
|
|
@@ -1415,9 +1430,17 @@ export const runEpicStories = (options) => Effect.gen(function* () {
|
|
|
1415
1430
|
target: flags.land,
|
|
1416
1431
|
rounds: yield* landRounds(plan.epicId, flags.land, rounds),
|
|
1417
1432
|
keepWorktrees: flags.keepWorktrees,
|
|
1418
|
-
gates: inAppDir(appDir, gatesIn(nodeProcessExecutor, events, commands)),
|
|
1433
|
+
gates: inAppDir(appDir, gatesIn(nodeProcessExecutor, events, commands, gateRunOptions(process.env))),
|
|
1419
1434
|
system: ["House rules of the target repository (CONTRIBUTING.md):", guidance].join("\n")
|
|
1420
1435
|
});
|
|
1436
|
+
// The gates' memory goes with the landed epic: baselines are
|
|
1437
|
+
// reproducible, logs keep only their failing lines (ADR 0027).
|
|
1438
|
+
const gateArtifacts = yield* compactGateArtifacts(files, stateDir, plan.stories.map((story) => story.id), commands);
|
|
1439
|
+
if (gateArtifacts.baselines > 0 || gateArtifacts.logs > 0) {
|
|
1440
|
+
yield* events.publish(Info.make({
|
|
1441
|
+
message: `epic ${plan.epicId}: removed ${gateArtifacts.baselines} gate baseline file(s), compacted ${gateArtifacts.logs} gate log(s) to their failing lines`
|
|
1442
|
+
}));
|
|
1443
|
+
}
|
|
1421
1444
|
// The epic's earlier runs' transcripts go with it; this run's may still be read.
|
|
1422
1445
|
const earlier = (yield* readEpicRuns(files, stateDir))
|
|
1423
1446
|
.map((run) => run.runId)
|
|
@@ -1457,7 +1480,8 @@ export const runEpicStories = (options) => Effect.gen(function* () {
|
|
|
1457
1480
|
: [
|
|
1458
1481
|
`The application lives in ${appDir}/ — its package.json, sources and tests; the gates run there.`
|
|
1459
1482
|
];
|
|
1460
|
-
const gates = inAppDir(appDir, gatesIn(nodeProcessExecutor, events, commands));
|
|
1483
|
+
const gates = inAppDir(appDir, gatesIn(nodeProcessExecutor, events, commands, gateRunOptions(process.env)));
|
|
1484
|
+
const testCommand = testGateCommand(commands);
|
|
1461
1485
|
const setupCommand = worktreeSetupCommand(process.env);
|
|
1462
1486
|
const healthUrl = serverHealthUrl(localServer, process.env);
|
|
1463
1487
|
const report = yield* implementStoriesFlow({ ...context, reasoning: reasoningMeter.service }, {
|
|
@@ -1488,6 +1512,23 @@ export const runEpicStories = (options) => Effect.gen(function* () {
|
|
|
1488
1512
|
setupAgent: setupAgentEnabled(process.env)
|
|
1489
1513
|
}),
|
|
1490
1514
|
gates,
|
|
1515
|
+
// Gates with a memory (ADR 0027): a story is charged only with
|
|
1516
|
+
// the failing lines it added to the epic head it started from.
|
|
1517
|
+
gateCommands: commands,
|
|
1518
|
+
appDir,
|
|
1519
|
+
...(testCommand === undefined
|
|
1520
|
+
? {}
|
|
1521
|
+
: {
|
|
1522
|
+
testGate: inAppDir(appDir, gatesIn(nodeProcessExecutor, events, [testCommand], gateRunOptions(process.env)))
|
|
1523
|
+
}),
|
|
1524
|
+
fix: fixPromptOptions(process.env),
|
|
1525
|
+
// A task's `verified:` claims are checked against the tool calls
|
|
1526
|
+
// its transcript shows (ADR 0027 decision 6); no transcript, no check.
|
|
1527
|
+
toolCalls: (story, since) => context.trace === undefined
|
|
1528
|
+
? Effect.succeed(undefined)
|
|
1529
|
+
: Effect.map(loadTranscript(nodeTranscriptFiles, join(input.workDir, ".llm4ts", "transcripts", context.trace.runId), { lane: story.id }), (entries) => entries === undefined
|
|
1530
|
+
? undefined
|
|
1531
|
+
: entries.flatMap((entry) => entry._tag === "Tool" && entry.at >= since ? [entry.args] : [])),
|
|
1491
1532
|
// With a roster, the judge and the verifier are leased per story,
|
|
1492
1533
|
// away from the executor coding it (ADR 0019).
|
|
1493
1534
|
...storyContextChars(process.env),
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// Run: modernize-implement --repo ~/services/meridian-transfers
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import * as Effect from "effect/Effect";
|
|
23
|
+
import { withContract } from "@llm4ts/flow/AutonomyContract";
|
|
23
24
|
import { Dimension, Sample } from "@llm4ts/core/eval/Eval";
|
|
24
25
|
import { judge } from "@llm4ts/core/eval/Judge";
|
|
25
26
|
import { FlowAborted, FlowLlmError, Info, ReviewResult, asReadOnly, coderFromEnv, implementTaskLoop, lintCommand, loadKitPatternCards, makeChat, makeNodeWorkspace, makePlanStore, mergeReviewResults, minimalReviewers, nodePlainFileStore, nodeProcessExecutor, openPack, resolveFlowInput, reviewAndFixLoop, reviewFingerprint, runFlowMain, runNode, stage } from "@llm4ts/runner";
|
|
@@ -246,7 +247,7 @@ const program = Effect.gen(function* () {
|
|
|
246
247
|
// tasks.
|
|
247
248
|
yield* implementTaskLoop(store, context.events, planPath, plan, (task) => Effect.gen(function* () {
|
|
248
249
|
const testsTask = task.title === firstTitle;
|
|
249
|
-
const coderChat = yield* makeChat(context.coder, { system });
|
|
250
|
+
const coderChat = yield* makeChat(context.coder, { system: withContract(system) });
|
|
250
251
|
yield* coderChat.ask(plan.taskPrompt(task));
|
|
251
252
|
yield* reviewAndFixLoop({
|
|
252
253
|
reviewers: [...minimalReviewers, ...pack.lenses],
|
|
@@ -263,6 +264,11 @@ const program = Effect.gen(function* () {
|
|
|
263
264
|
: testGate === undefined
|
|
264
265
|
? {}
|
|
265
266
|
: { lint: testGate }),
|
|
267
|
+
oracle: {
|
|
268
|
+
diff: context.git.diffAll,
|
|
269
|
+
declared: testsTask,
|
|
270
|
+
...(pack.oracle === undefined ? {} : { rules: pack.oracle })
|
|
271
|
+
},
|
|
266
272
|
parallelism: 1
|
|
267
273
|
});
|
|
268
274
|
if (testsTask && testGate !== undefined) {
|
package/flows/sdd.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Spec-driven development: write a specification, encode it as red tests, implement to green.
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import { withContract } from "@llm4ts/flow/AutonomyContract";
|
|
4
5
|
import { FlowAborted, Plan, asReadOnly, coderFromEnv, defaultPlanInstructions, defaultPlanPath, implementTaskLoop, lintCommand, makeChat, makePlanStore, minimalReviewers, nodePlainFileStore, nodeProcessExecutor, planFrom, resolveFlowInput, reviewAndFixLoop, runFlowMain, runNode, stage, withModel, writeBrief } from "@llm4ts/runner";
|
|
5
6
|
import { gemini } from "@llm4ts/runner/Connectors";
|
|
6
7
|
const proModel = process.env.LLM4TS_REASONING_MODEL ?? "gemini-3-pro-preview";
|
|
@@ -70,7 +71,7 @@ const program = Effect.gen(function* () {
|
|
|
70
71
|
.pipe(Effect.andThen(context.git.commitAll(`${planWithSpec.epicId}: specification`)), Effect.asVoid)
|
|
71
72
|
: Effect.void)));
|
|
72
73
|
const coderChat = yield* makeChat(context.coder, {
|
|
73
|
-
system: "Implement one task at a time. The committed specification is the contract; do not weaken its tests."
|
|
74
|
+
system: withContract("Implement one task at a time. The committed specification is the contract; do not weaken its tests.")
|
|
74
75
|
});
|
|
75
76
|
const testGate = lintCommand(nodeProcessExecutor, context.events, ["mvn", "-q", "test"], input.workDir);
|
|
76
77
|
const compileGate = lintCommand(nodeProcessExecutor, context.events, ["mvn", "-q", "test-compile"], input.workDir);
|
|
@@ -86,6 +87,9 @@ const program = Effect.gen(function* () {
|
|
|
86
87
|
currentDiff: context.git.diffAll,
|
|
87
88
|
events: context.events,
|
|
88
89
|
lint: testsTask ? compileGate : testGate,
|
|
90
|
+
// The red tests are the contract: the first task may write
|
|
91
|
+
// them, no later task may delete or skip them (ADR 0027).
|
|
92
|
+
oracle: { diff: context.git.diffAll, declared: testsTask },
|
|
89
93
|
parallelism: 1
|
|
90
94
|
});
|
|
91
95
|
if (testsTask) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/shell",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.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",
|
|
54
54
|
"@effect/platform-node-shared": "4.0.0",
|
|
55
|
-
"@llm4ts/core": "2.
|
|
56
|
-
"@llm4ts/
|
|
57
|
-
"@llm4ts/
|
|
55
|
+
"@llm4ts/core": "2.31.0",
|
|
56
|
+
"@llm4ts/runner": "2.31.0",
|
|
57
|
+
"@llm4ts/flow": "2.31.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"effect": "4.0.0"
|