@llm4ts/shell 2.44.1 → 2.45.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.
@@ -40,7 +40,7 @@ import { estimatedUsageOptionsFromEnv, makeEstimatedUsageMeter } from "@llm4ts/f
40
40
  import { landEpic } from "@llm4ts/flow/Landing";
41
41
  import { deferredPath, implementStoriesFlow } from "@llm4ts/flow/Stories";
42
42
  import { validateStoryPlan } from "@llm4ts/flow/StoryPlan";
43
- import { asReadOnly, nodePlainFileStore, nodeProcessExecutor, resolveFlowInput, runNode, stage, withModel } from "@llm4ts/runner";
43
+ import { asReadOnly, nodePlainFileStore, nodeProcessExecutor, openPack, resolveFlowInput, runNode, stage, withModel } from "@llm4ts/runner";
44
44
  import { claude, coderIds, judgmentConnectorFromEnvironment, pi } from "@llm4ts/runner/Connectors";
45
45
  import { FlowAborted, FlowLlmError, ReviewResult, ScriptUsage, coderFor, lintCommand } from "@llm4ts/runner";
46
46
  export const epicUsage = [
@@ -73,9 +73,11 @@ export const epicUsage = [
73
73
  "Seats: LLM4TS_REASONER (claude|gemini|…, default claude) splits, reviews, judges;",
74
74
  " LLM4TS_CODER (default pi) implements; LLM4TS_REASONING_MODEL / LLM4TS_CODER_MODEL",
75
75
  " pick their models (pi: provider/model); LLM4TS_CODER_FLAGS / LLM4TS_REASONING_FLAGS",
76
- " add CLI flags (key=value;key=value). LLM4TS_GATES overrides the gate commands",
77
- " (default: those of pnpm typecheck|lint|test|build the app's package.json defines);",
78
- " LLM4TS_WORKTREE_SETUP (default: pnpm install --offline) prepares each worktree;",
76
+ " add CLI flags (key=value;key=value). LLM4TS_PACK (or llm4ts run --pack) uses",
77
+ " that pack's gates; LLM4TS_GATES overrides them (otherwise: pnpm typecheck|lint|",
78
+ " test|build, each only when the app's package.json defines that script);",
79
+ " LLM4TS_WORKTREE_SETUP prepares each worktree (default: the pack's setup:, or",
80
+ " pnpm install --offline with no pack; empty: none);",
79
81
  " LLM4TS_WORKTREE_ROOT (default: <repo>.worktrees beside the repository) holds them;",
80
82
  " LLM4TS_APP_DIR (e.g. frontend) runs setup and gates in that subfolder; unset, a",
81
83
  " repository without a root package.json uses its one subfolder that has one;",
@@ -1119,13 +1121,15 @@ export const inAppDir = (appDir, step) => (workDir, ...rest) => step(appDir ===
1119
1121
  export const defaultWorktreeSetup = ["pnpm", "install", "--offline"];
1120
1122
  /**
1121
1123
  * `LLM4TS_WORKTREE_SETUP="pnpm install --offline"` (the default) prepares
1122
- * each story worktree; an empty value disables the step. Offline by default:
1123
- * the runbook warms the pnpm store once, and a workshop stage has no network.
1124
+ * each story worktree; an empty value disables the step. Unset, a selected
1125
+ * pack's `setup:` is the command, and a pack without one runs none: its
1126
+ * target is not assumed to be a pnpm project. Offline by default: the
1127
+ * runbook warms the pnpm store once, and a workshop stage has no network.
1124
1128
  */
1125
- export const worktreeSetupCommand = (environment) => {
1129
+ export const worktreeSetupCommand = (environment, pack) => {
1126
1130
  const raw = environment.LLM4TS_WORKTREE_SETUP;
1127
1131
  if (raw === undefined) {
1128
- return defaultWorktreeSetup;
1132
+ return pack === undefined ? defaultWorktreeSetup : pack.setup;
1129
1133
  }
1130
1134
  const parts = raw
1131
1135
  .trim()
@@ -1166,25 +1170,67 @@ export const appScripts = (files, appRoot) => files.read(join(appRoot, "package.
1166
1170
  ? Effect.succeed(undefined)
1167
1171
  : Effect.map(Schema.decodeUnknownEffect(PackageScripts)(text), (manifest) => new Set(Object.keys(manifest.scripts ?? {})))), Effect.catch(() => Effect.succeed(undefined)));
1168
1172
  /**
1169
- * `LLM4TS_GATES="pnpm typecheck;pnpm test"` overrides the four defaults.
1170
- * Without it, a default gate runs only when the application defines that
1171
- * script: a Next.js app has `lint` and `build` but seldom `typecheck`, and
1173
+ * `LLM4TS_GATES="pnpm typecheck;pnpm test"` wins. Otherwise `packGates` —
1174
+ * the selected pack's `## Gates`, in manifest order — are the commands, and
1175
+ * an empty list stays empty (it is not filled with the defaults). With no
1176
+ * pack, a default gate runs only when the application defines that script:
1177
+ * a Next.js app has `lint` and `build` but seldom `typecheck`, and
1172
1178
  * `pnpm typecheck` there fails with "Command not found", not a red gate.
1173
1179
  */
1174
- export const gateCommands = (environment, scripts) => {
1180
+ export const gateCommands = (environment, scripts, packGates) => {
1175
1181
  const raw = environment.LLM4TS_GATES?.trim();
1176
- if (raw === undefined || raw.length === 0) {
1177
- return scripts === undefined
1178
- ? defaultGateCommands
1179
- : defaultGateCommands.filter((command) => scripts.has(command[1] ?? ""));
1182
+ if (raw !== undefined && raw.length > 0) {
1183
+ return raw
1184
+ .split(";")
1185
+ .map((command) => command
1186
+ .trim()
1187
+ .split(/\s+/)
1188
+ .filter((part) => part.length > 0))
1189
+ .filter((command) => command.length > 0);
1180
1190
  }
1181
- return raw
1182
- .split(";")
1183
- .map((command) => command
1184
- .trim()
1185
- .split(/\s+/)
1186
- .filter((part) => part.length > 0))
1187
- .filter((command) => command.length > 0);
1191
+ if (packGates !== undefined) {
1192
+ return packGates;
1193
+ }
1194
+ return scripts === undefined
1195
+ ? defaultGateCommands
1196
+ : defaultGateCommands.filter((command) => scripts.has(command[1] ?? ""));
1197
+ };
1198
+ /**
1199
+ * The pack `LLM4TS_PACK` names (what `llm4ts run --pack` forwards), opened
1200
+ * before any seat is resolved so a wrong name fails before the planner
1201
+ * spends anything. Unset, there is none — a pack is never implied:
1202
+ * `openPack` would otherwise load `cobol-springboot`, which is the
1203
+ * modernization flows' default, not an epic's.
1204
+ */
1205
+ export const selectedPack = (options) => (options.environment.LLM4TS_PACK?.trim() ?? "").length === 0
1206
+ ? Effect.succeed(undefined)
1207
+ : Effect.map(openPack({
1208
+ environment: options.environment,
1209
+ launchDir: options.launchDir,
1210
+ flowDir: options.flowDir
1211
+ }), (opened) => opened.pack);
1212
+ /** The run's gates: `LLM4TS_GATES`, else the selected pack's `## Gates`, else the defaults. */
1213
+ export const epicGateCommands = (options) => {
1214
+ const override = (options.environment.LLM4TS_GATES?.trim() ?? "").length > 0;
1215
+ return {
1216
+ commands: gateCommands(options.environment, options.scripts, options.pack === undefined
1217
+ ? undefined
1218
+ : (options.pack.gateList ?? Object.values(options.pack.gates))),
1219
+ source: override ? "override" : options.pack === undefined ? "defaults" : "pack",
1220
+ ...(options.pack === undefined ? {} : { packName: options.pack.name })
1221
+ };
1222
+ };
1223
+ /** The line the run prints before the first gate. */
1224
+ export const gateListMessage = (resolved) => {
1225
+ if (resolved.commands.length === 0) {
1226
+ return resolved.source === "pack"
1227
+ ? `gates: none (pack '${resolved.packName ?? ""}' defines none; set LLM4TS_GATES)`
1228
+ : "gates: none (the app defines none of typecheck, lint, test, build; set LLM4TS_GATES)";
1229
+ }
1230
+ const listed = resolved.commands.map((command) => command.join(" ")).join(" · ");
1231
+ return resolved.source === "pack"
1232
+ ? `gates: ${listed} (pack ${resolved.packName})`
1233
+ : `gates: ${listed}`;
1188
1234
  };
1189
1235
  /** The gate runner lives in `@llm4ts/flow/Gates` (ADR 0027); re-exported for forks and tests. */
1190
1236
  export { gatesIn };
@@ -1421,6 +1467,12 @@ export const runEpicStories = (options) => Effect.gen(function* () {
1421
1467
  ? choice.request
1422
1468
  : choice.prompt
1423
1469
  };
1470
+ // The pack whose `## Gates` the run uses; a wrong name fails here, not after planning.
1471
+ const pack = yield* selectedPack({
1472
+ environment: process.env,
1473
+ launchDir: given.workspace,
1474
+ flowDir: import.meta.dirname
1475
+ });
1424
1476
  const coderFlags = flagsFromEnvironment(process.env.LLM4TS_CODER_FLAGS);
1425
1477
  const reasoning = withExtraFlags(withOptionalModel(yield* reasonerFromEnvironment(process.env), process.env.LLM4TS_REASONING_MODEL), flagsFromEnvironment(process.env.LLM4TS_REASONING_FLAGS));
1426
1478
  const coder = withExtraFlags(withOptionalModel(yield* storyCoderFromEnvironment(process.env), process.env.LLM4TS_CODER_MODEL), coderFlags);
@@ -1571,12 +1623,13 @@ export const runEpicStories = (options) => Effect.gen(function* () {
1571
1623
  message: `app dir: ${appDir} (setup and gates run there; LLM4TS_APP_DIR)`
1572
1624
  }));
1573
1625
  }
1574
- const commands = gateCommands(process.env, yield* appScripts(files, join(input.workDir, appDir)));
1575
- yield* events.publish(Info.make({
1576
- message: commands.length === 0
1577
- ? "gates: none (the app defines none of typecheck, lint, test, build; set LLM4TS_GATES)"
1578
- : `gates: ${commands.map((command) => command.join(" ")).join(" · ")}`
1579
- }));
1626
+ const resolvedGates = epicGateCommands({
1627
+ environment: process.env,
1628
+ scripts: yield* appScripts(files, join(input.workDir, appDir)),
1629
+ pack
1630
+ });
1631
+ const commands = resolvedGates.commands;
1632
+ yield* events.publish(Info.make({ message: gateListMessage(resolvedGates) }));
1580
1633
  // The Node the gates would run on is the one on PATH, not the one
1581
1634
  // the application pins; a mismatch is a red first story that says
1582
1635
  // nothing about Node. Ask now, and name both.
@@ -1644,7 +1697,7 @@ export const runEpicStories = (options) => Effect.gen(function* () {
1644
1697
  ];
1645
1698
  const gates = inAppDir(appDir, gatesIn(nodeProcessExecutor, events, commands, gateRunOptions(process.env)));
1646
1699
  const testCommand = testGateCommand(commands);
1647
- const setupCommand = worktreeSetupCommand(process.env);
1700
+ const setupCommand = worktreeSetupCommand(process.env, pack);
1648
1701
  const healthUrl = serverHealthUrl(localServer, process.env);
1649
1702
  // The executor over one unit: the epic's plan or one refine round.
1650
1703
  const runUnit = (work) => implementStoriesFlow({ ...context, reasoning: reasoningMeter.service }, {
@@ -9,6 +9,7 @@ specs-dir: docs/modernization/specs
9
9
  features-dir: docs/modernization/features
10
10
  program-files: (?:src/app/<NAME>(?:/.*)?|src/services/<NAME>(?:/.*)?|contracts/<NAME>\.openapi\.yaml|tests/<NAME>\..*)
11
11
  feature-files: (?:src/services/<NAME>(?:/.*)?|contracts/<NAME>\.openapi\.yaml)
12
+ setup: pnpm install --offline
12
13
 
13
14
  ## Gates
14
15
 
@@ -6,6 +6,7 @@ sources: .*\.(jsp|java|xml)
6
6
  programs: .*\.(jsp|java)
7
7
  specs-dir: docs/specs
8
8
  features-dir: features
9
+ setup: npm install --offline
9
10
 
10
11
  ## Gates
11
12
 
@@ -7,6 +7,7 @@ target: {{dir}}/{{base}}.ts
7
7
  comment: //
8
8
  specs-dir: docs/port
9
9
  features-dir: docs/port/features
10
+ setup: pnpm install --offline
10
11
 
11
12
  ## Gates
12
13
 
@@ -112,10 +112,13 @@ Review `.llm4ts/epics/<epic-id>/plan.md` in the ACE repository (edit it to
112
112
  re-plan), then run the command `plan` printed:
113
113
 
114
114
  ```bash
115
- LLM4TS_GATES="bash scripts/ace-gates.sh build;bash scripts/ace-gates.sh test" LLM4TS_WORKTREE_SETUP= \
116
- llm4ts run epic-stories --repo ~/work/conti-ace -- --concurrency 2 "<the epic sentence>"
115
+ llm4ts run epic-stories --pack ace12-rest --repo ~/work/conti-ace -- \
116
+ --concurrency 2 "<the epic sentence>"
117
117
  ```
118
118
 
119
+ The pack supplies the gates (`bash scripts/ace-gates.sh build` and `test`)
120
+ and declares no `setup:`, so the story worktrees skip `pnpm install`.
121
+
119
122
  Each story lands on its own branch and merges into `epic/<epic-id>` after its
120
123
  gates and judge pass; nothing reaches `main` without a human. The board and
121
124
  report live under `.llm4ts/epics/<epic-id>/`. Azure DevOps mirroring is the
@@ -196,8 +196,7 @@ const program = Effect.gen(function* () {
196
196
  }
197
197
  yield* say([
198
198
  "next: review the plan, commit the seed in the ACE repository, then run",
199
- ` LLM4TS_GATES="bash scripts/ace-gates.sh build;bash scripts/ace-gates.sh test" LLM4TS_WORKTREE_SETUP= \\`,
200
- ` llm4ts run epic-stories --repo ${targetRoot} -- --concurrency 2 ${JSON.stringify(epicSentence(file.design, api, service))}`
199
+ ` llm4ts run epic-stories --pack ace12-rest --repo ${targetRoot} -- --concurrency 2 ${JSON.stringify(epicSentence(file.design, api, service))}`
201
200
  ].join("\n"));
202
201
  }));
203
202
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "2.44.1",
3
+ "version": "2.45.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.44.1",
56
- "@llm4ts/flow": "2.44.1",
57
- "@llm4ts/runner": "2.44.1"
55
+ "@llm4ts/flow": "2.45.0",
56
+ "@llm4ts/core": "2.45.0",
57
+ "@llm4ts/runner": "2.45.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "effect": "4.0.0"