@llm4ts/shell 2.11.0 → 2.12.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-stories.js +19 -5
- package/flows/lib/epic-stories.js +52 -4
- package/package.json +4 -4
package/flows/epic-stories.js
CHANGED
|
@@ -29,7 +29,7 @@ import { landEpic } from "@llm4ts/flow/Landing";
|
|
|
29
29
|
import { implementStoriesFlow } from "@llm4ts/flow/Stories";
|
|
30
30
|
import { makeStoryPlanStore, validateStoryPlan } from "@llm4ts/flow/StoryPlan";
|
|
31
31
|
import * as Console from "effect/Console";
|
|
32
|
-
import { awaitServer, chooseEpic, epicsDir, listEpics, renderEpicList, combineTotals, epicIdFor, flagsFromEnvironment, gateCommands, gatesIn, generateStoryPlan, httpProbe, judgeStory, localCoderServer, parseEpicArgs, reasonerFromEnvironment, serverHealthUrl, setupIn, storyCoderFromEnvironment, verifyBlockedOn, worktreeRootFor, worktreeSetupCommand } from "./lib/epic-stories.js";
|
|
32
|
+
import { awaitServer, chooseEpic, epicsDir, listEpics, renderEpicList, combineTotals, epicIdFor, flagsFromEnvironment, appDirFor, gateCommands, gatesIn, inAppDir, generateStoryPlan, httpProbe, judgeStory, localCoderServer, parseEpicArgs, reasonerFromEnvironment, serverHealthUrl, setupAgentEnabled, setupIn, storyCoderFromEnvironment, verifyBlockedOn, worktreeRootFor, worktreeSetupCommand } from "./lib/epic-stories.js";
|
|
33
33
|
/** `LLM4TS_CODER_MODEL` / `LLM4TS_REASONING_MODEL`: pi takes `provider/model` (e.g. `openai-codex/gpt-5.5`). */
|
|
34
34
|
const withOptionalModel = (config, model) => {
|
|
35
35
|
const trimmed = model?.trim();
|
|
@@ -95,6 +95,11 @@ const program = Effect.gen(function* () {
|
|
|
95
95
|
if (flags.planOnly) {
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
|
+
// Setup and gates run where the application is, in every checkout.
|
|
99
|
+
const appDir = yield* appDirFor(input.workDir, process.env);
|
|
100
|
+
if (appDir !== ".") {
|
|
101
|
+
yield* events.publish(Info.make({ message: `app dir: ${appDir} (setup and gates run there; LLM4TS_APP_DIR)` }));
|
|
102
|
+
}
|
|
98
103
|
if (flags.land !== undefined) {
|
|
99
104
|
const landed = yield* landEpic(context, {
|
|
100
105
|
plan,
|
|
@@ -102,7 +107,7 @@ const program = Effect.gen(function* () {
|
|
|
102
107
|
stateDir,
|
|
103
108
|
target: flags.land,
|
|
104
109
|
keepWorktrees: flags.keepWorktrees,
|
|
105
|
-
gates: gatesIn(nodeProcessExecutor, events, gateCommands(process.env)),
|
|
110
|
+
gates: inAppDir(appDir, gatesIn(nodeProcessExecutor, events, gateCommands(process.env))),
|
|
106
111
|
system: ["House rules of the target repository (CONTRIBUTING.md):", guidance].join("\n")
|
|
107
112
|
});
|
|
108
113
|
yield* events.publish(Info.make({
|
|
@@ -126,7 +131,12 @@ const program = Effect.gen(function* () {
|
|
|
126
131
|
message: "this runner cannot rebind seats to a worktree (no contextFor)"
|
|
127
132
|
});
|
|
128
133
|
}
|
|
129
|
-
const
|
|
134
|
+
const appDirNote = appDir === "."
|
|
135
|
+
? []
|
|
136
|
+
: [
|
|
137
|
+
`The application lives in ${appDir}/ — its package.json, sources and tests; the gates run there.`
|
|
138
|
+
];
|
|
139
|
+
const gates = inAppDir(appDir, gatesIn(nodeProcessExecutor, events, gateCommands(process.env)));
|
|
130
140
|
const setupCommand = worktreeSetupCommand(process.env);
|
|
131
141
|
const healthUrl = serverHealthUrl(localServer, process.env);
|
|
132
142
|
const report = yield* implementStoriesFlow({ ...context, reasoning: reasoningMeter.service }, {
|
|
@@ -151,7 +161,10 @@ const program = Effect.gen(function* () {
|
|
|
151
161
|
}),
|
|
152
162
|
...(setupCommand === undefined
|
|
153
163
|
? {}
|
|
154
|
-
: {
|
|
164
|
+
: {
|
|
165
|
+
setup: inAppDir(appDir, setupIn(nodeProcessExecutor, events, setupCommand)),
|
|
166
|
+
setupAgent: setupAgentEnabled(process.env)
|
|
167
|
+
}),
|
|
155
168
|
gates,
|
|
156
169
|
// With a roster, the judge and the verifier are leased per story,
|
|
157
170
|
// away from the executor coding it (ADR 0019).
|
|
@@ -166,7 +179,8 @@ const program = Effect.gen(function* () {
|
|
|
166
179
|
"House rules of the target repository (CONTRIBUTING.md):",
|
|
167
180
|
guidance,
|
|
168
181
|
"",
|
|
169
|
-
`Imitate the exemplar feature before inventing anything. Story id: ${story.id}
|
|
182
|
+
`Imitate the exemplar feature before inventing anything. Story id: ${story.id}.`,
|
|
183
|
+
...appDirNote
|
|
170
184
|
].join("\n")),
|
|
171
185
|
concurrency,
|
|
172
186
|
failFast: flags.failFast
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// story-plan generator prompt and schema, the story judge, the gate runner,
|
|
3
3
|
// and seat selection. The executor itself is `@llm4ts/flow/Stories`.
|
|
4
4
|
import { readdir } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
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
8
|
import { Dimension, Sample } from "@llm4ts/core/eval/Eval";
|
|
@@ -37,6 +37,9 @@ export const epicUsage = [
|
|
|
37
37
|
" add CLI flags (key=value;key=value). LLM4TS_GATES overrides the gate commands;",
|
|
38
38
|
" LLM4TS_WORKTREE_SETUP (default: pnpm install --offline) prepares each worktree;",
|
|
39
39
|
" LLM4TS_WORKTREE_ROOT (default: <repo>.worktrees beside the repository) holds them;",
|
|
40
|
+
" LLM4TS_APP_DIR (e.g. frontend) runs setup and gates in that subfolder; unset, a",
|
|
41
|
+
" repository without a root package.json uses its one subfolder that has one;",
|
|
42
|
+
" LLM4TS_SETUP_AGENT=1 gives the coder one turn to fix a failed setup, then reruns it;",
|
|
40
43
|
" LLM4TS_CODER_HEALTH_URL is polled after the coder's serving engine goes down."
|
|
41
44
|
].join("\n");
|
|
42
45
|
/** The flow's own flags, taken out before the shared `--repo`/prompt parsing sees the rest. */
|
|
@@ -544,6 +547,45 @@ export const judgeStory = (reasoning, story, diff, budget, plan) => judge(reason
|
|
|
544
547
|
response: cap(diff, budget).text
|
|
545
548
|
}))
|
|
546
549
|
.pipe(Effect.mapError(FlowLlmError.from), Effect.map((scored) => subBar(scored, story)));
|
|
550
|
+
// ---- App directory ----------------------------------------------------------------
|
|
551
|
+
/** Folders never taken for the application: dependencies, build output, llm4ts state. */
|
|
552
|
+
const ignoredAppDirs = new Set(["node_modules", "dist", "build", "out", "coverage"]);
|
|
553
|
+
const hasPackageJson = (dir) => Effect.tryPromise(() => readdir(dir)).pipe(Effect.map((names) => names.includes("package.json")), Effect.catch(() => Effect.succeed(false)));
|
|
554
|
+
/**
|
|
555
|
+
* Where setup and gates run, relative to a checkout: `LLM4TS_APP_DIR` when set
|
|
556
|
+
* (`.` for the root); otherwise the root when it has a `package.json`, else
|
|
557
|
+
* its ONE first-level subfolder that has one (a Next.js app in `frontend/`).
|
|
558
|
+
* Several candidates, or none, keep the root — the setup error then names it.
|
|
559
|
+
* The value is relative so it applies to every worktree and the epic checkout.
|
|
560
|
+
*/
|
|
561
|
+
export const appDirFor = (workDir, environment) => Effect.gen(function* () {
|
|
562
|
+
const configured = environment.LLM4TS_APP_DIR?.trim();
|
|
563
|
+
if (configured !== undefined && configured.length > 0) {
|
|
564
|
+
const normalized = normalize(configured);
|
|
565
|
+
const relative = normalized.replace(/[\\/]+$/, "");
|
|
566
|
+
if (isAbsolute(normalized) || relative === ".." || /^\.\.[\\/]/.test(relative)) {
|
|
567
|
+
return yield* ScriptUsage.make({
|
|
568
|
+
message: `LLM4TS_APP_DIR must be a folder inside the repository, got '${configured}'`
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return relative.length === 0 ? "." : relative;
|
|
572
|
+
}
|
|
573
|
+
if (yield* hasPackageJson(workDir)) {
|
|
574
|
+
return ".";
|
|
575
|
+
}
|
|
576
|
+
const entries = yield* Effect.tryPromise(() => readdir(workDir, { withFileTypes: true })).pipe(Effect.catch(() => Effect.succeed([])));
|
|
577
|
+
const candidates = [];
|
|
578
|
+
for (const entry of entries) {
|
|
579
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && !ignoredAppDirs.has(entry.name)) {
|
|
580
|
+
if (yield* hasPackageJson(join(workDir, entry.name))) {
|
|
581
|
+
candidates.push(entry.name);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return candidates.length === 1 ? (candidates[0] ?? ".") : ".";
|
|
586
|
+
});
|
|
587
|
+
/** Runs a per-checkout step in the app directory of whichever checkout it is given. */
|
|
588
|
+
export const inAppDir = (appDir, step) => (workDir) => step(appDir === "." ? workDir : join(workDir, appDir));
|
|
547
589
|
// ---- Worktree setup --------------------------------------------------------------
|
|
548
590
|
export const defaultWorktreeSetup = ["pnpm", "install", "--offline"];
|
|
549
591
|
/**
|
|
@@ -562,13 +604,19 @@ export const worktreeSetupCommand = (environment) => {
|
|
|
562
604
|
.filter((part) => part.length > 0);
|
|
563
605
|
return parts.length === 0 ? undefined : parts;
|
|
564
606
|
};
|
|
607
|
+
/**
|
|
608
|
+
* `LLM4TS_SETUP_AGENT=1` (or `true`/`on`): a failed setup gets one coder turn
|
|
609
|
+
* to make the worktree ready, then runs again as the check. Off by default.
|
|
610
|
+
*/
|
|
611
|
+
export const setupAgentEnabled = (environment) => /^(1|true|on|yes)$/i.test(environment.LLM4TS_SETUP_AGENT?.trim() ?? "");
|
|
612
|
+
const output = (result) => result.issues.map((issue) => issue.description).join("\n");
|
|
565
613
|
/** Runs the setup command in a worktree; a non-zero exit fails the story with the output. */
|
|
566
614
|
export const setupIn = (process, events, command) => (workDir) => Effect.flatMap(lintCommand(process, events, command, workDir), (result) => result.isClean
|
|
567
615
|
? Effect.void
|
|
568
616
|
: FlowAborted.make({
|
|
569
|
-
message: `worktree setup failed (${command.join(" ")}):\n${result
|
|
570
|
-
|
|
571
|
-
|
|
617
|
+
message: `worktree setup failed (${command.join(" ")}):\n${output(result)}${/ERR_PNPM_NO_PKG_MANIFEST|ENOENT.*package\.json/.test(output(result))
|
|
618
|
+
? "\n(the application is not at this folder: set LLM4TS_APP_DIR to its subfolder)"
|
|
619
|
+
: ""}`
|
|
572
620
|
}));
|
|
573
621
|
// ---- Gates ---------------------------------------------------------------------
|
|
574
622
|
export const defaultGateCommands = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/shell",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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/flow": "2.
|
|
57
|
-
"@llm4ts/runner": "2.
|
|
55
|
+
"@llm4ts/core": "2.12.0",
|
|
56
|
+
"@llm4ts/flow": "2.12.0",
|
|
57
|
+
"@llm4ts/runner": "2.12.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"effect": "4.0.0-rc.115"
|