@llm4ts/shell 2.24.0 → 2.25.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.
@@ -15,6 +15,7 @@ import { parseEpicBrief, renderEpicBrief, unanswered } from "@llm4ts/flow/EpicBr
15
15
  import { structuredAndPublish } from "@llm4ts/flow/Flow";
16
16
  import { EpicBriefInvalid, EpicBriefNotApproved, EpicCheckoutDirty, LandingFailed, RefineRefused } from "@llm4ts/flow/FlowError";
17
17
  import { Info } from "@llm4ts/flow/FlowEvents";
18
+ import { nodePreflight } from "@llm4ts/flow/NodePreflight";
18
19
  import { statusPaths } from "@llm4ts/flow/GitTool";
19
20
  import { EpicLanded, EpicLandedVersion, landedPath } from "@llm4ts/flow/Landing";
20
21
  import { RefineProposal, assembleRound, countNotPlanned, earlierStories, refineProposalJsonSchema, renderNotPlanned, roundPrefix, roundRepair, runAction } from "@llm4ts/flow/RefineRound";
@@ -1025,7 +1026,8 @@ export const storyContextChars = (environment) => {
1025
1026
  /** Today's story judge: the rubric judge over the four dimensions. */
1026
1027
  export const rubricStoryJudge = (context) => (story, diff, seats, subject) => judgeStory(
1027
1028
  // The story's own seat, not the run's: its time counts for the story.
1028
- seats.context.roster?.forRole("judge") ?? seats.context.reasoning, story, diff, context.budget, context.plan, subject);
1029
+ // And the judge seat, never the writing reasoner: a judge reads.
1030
+ seats.context.roster?.forRole("judge") ?? seats.context.judge ?? seats.context.reasoning, story, diff, context.budget, context.plan, subject);
1029
1031
  /** The runner options the two flags above add; empty when neither is set. */
1030
1032
  export const epicRunnerOptions = Effect.fn("epic-stories.runnerOptions")(function* (options, environment) {
1031
1033
  const judgment = options.judgmentFromEnvironment === true
@@ -1237,6 +1239,12 @@ export const runEpicStories = (options) => Effect.gen(function* () {
1237
1239
  ? "gates: none (the app defines none of typecheck, lint, test, build; set LLM4TS_GATES)"
1238
1240
  : `gates: ${commands.map((command) => command.join(" ")).join(" · ")}`
1239
1241
  }));
1242
+ // The Node the gates would run on is the one on PATH, not the one
1243
+ // the application pins; a mismatch is a red first story that says
1244
+ // nothing about Node. Ask now, and name both.
1245
+ if (commands.length > 0) {
1246
+ yield* nodePreflight(nodeProcessExecutor, files, events, join(input.workDir, appDir), process.env);
1247
+ }
1240
1248
  if (flags.land !== undefined) {
1241
1249
  const landed = yield* landEpic(context, {
1242
1250
  plan,
@@ -29,6 +29,7 @@ import { judgeAllPrograms } from "@llm4ts/flow/ProgramJudge";
29
29
  import { Provenance, makeProvenanceStore } from "@llm4ts/flow/Provenance";
30
30
  import { ApprovedMarker, DraftApprovalMarker, requireApproval } from "@llm4ts/flow/Approval";
31
31
  import { loadPatternCards, taggedPatternIds } from "@llm4ts/flow/Patterns";
32
+ import { nodePreflight } from "@llm4ts/flow/NodePreflight";
32
33
  import { ReviewIssue } from "@llm4ts/flow/Review";
33
34
  import { checkWall, wallBreachMessage } from "@llm4ts/flow/Wall";
34
35
  import { parseDecisions } from "@llm4ts/flow/Decisions";
@@ -194,6 +195,8 @@ const program = Effect.gen(function* () {
194
195
  message: `no plan at ${planPath} — run modernize-seed first`
195
196
  });
196
197
  }
198
+ // The pack's gates run on the node on PATH: check it against the target's pin first.
199
+ yield* nodePreflight(nodeProcessExecutor, files, context.events, input.workDir, process.env);
197
200
  const gate = (name) => {
198
201
  const command = pack.gate(name);
199
202
  return command === undefined
@@ -28,3 +28,15 @@ LLM4TS_LEGACY_REPO=/path/to/legacy-estate llm4ts run convert-page --pack j2ee-ne
28
28
 
29
29
  The convert flows default to `j2ee-nextjs-spa`; `kits/test/packs.test.ts`
30
30
  shows what the flows require of a manifest.
31
+
32
+ ## Node for the gates
33
+
34
+ The Next.js scaffold pins Node 22 (`.nvmrc`, `package.json` `engines`). The
35
+ pack's gates run on the `node` on the PATH of the shell that launches
36
+ `llm4ts`, so `convert-*`, `modernize-implement` and `epic-stories` check it
37
+ against the target's pin before the first gate and stop, naming both, when
38
+ they disagree; `llm4ts doctor` reports the same under `gates:`. Switch Node
39
+ (`nvm use` / `fnm use`), or uncomment `use-node-version` in the target's
40
+ `.npmrc` so pnpm runs the pinned Node itself, or give the pack gates a
41
+ version-manager wrapper (`fnm exec --using=.nvmrc pnpm lint`). Set
42
+ `LLM4TS_NODE_CHECK=off` to run anyway.
@@ -18,10 +18,10 @@ node kits/j2ee-nextjs/fixtures/demo-bank/preflight.mjs
18
18
  ```
19
19
 
20
20
  Preflight seeds both fixtures into a temp dir, runs their smoke checks, and
21
- verifies git, pnpm, the `claude` CLI, and that the installed `llm4ts` ships
22
- the `j2ee-nextjs` kit with its `j2ee-nextjs-spa` pack (`@llm4ts/shell`
23
- 0.18.0 or newer). Fix anything red before continuing. Then materialize the
24
- demo estate (pick a short path you can type on stage):
21
+ verifies git, Node 22 on PATH, pnpm, the `claude` CLI, and that the installed
22
+ `llm4ts` ships the `j2ee-nextjs` kit with its `j2ee-nextjs-spa` pack
23
+ (`@llm4ts/shell` 0.18.0 or newer). Fix anything red before continuing. Then
24
+ materialize the demo estate (pick a short path you can type on stage):
25
25
 
26
26
  ```bash
27
27
  node kits/j2ee-nextjs/fixtures/demo-bank/reset-demo.mjs ~/demo
@@ -32,6 +32,17 @@ warms the target's node_modules from the pnpm store (documented offline
32
32
  strategy: the lockfile is committed; run it once on hotel wifi, never on
33
33
  stage). Finally: `cd ~/demo/nextjs && pnpm test` must be green.
34
34
 
35
+ **Node for the gates.** The target pins Node 22 (`.nvmrc`, `package.json`
36
+ `engines`). Gates run on the `node` found on the PATH of the shell that
37
+ launches `llm4ts`, so every gated flow checks that node against the pin
38
+ before its first gate and stops with both versions named when they
39
+ disagree (`llm4ts doctor` shows the same line under `gates:`). On a machine
40
+ whose Node you cannot switch (`nvm use 22` / `fnm use 22`), uncomment
41
+ `use-node-version` in the target's `.npmrc` so pnpm fetches and runs that
42
+ Node itself (one download, then offline), or wrap the pack's gate commands
43
+ in a version manager (`fnm exec --using=.nvmrc pnpm lint` works as a gate
44
+ line, since gates are plain argv). `LLM4TS_NODE_CHECK=off` skips the check.
45
+
35
46
  Timing bounds for the day (set in the environment you run flows from):
36
47
 
37
48
  ```bash
@@ -2,6 +2,9 @@
2
2
  "name": "demo-bank-nextjs",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
+ "engines": {
6
+ "node": ">=22"
7
+ },
5
8
  "scripts": {
6
9
  "dev": "next dev",
7
10
  "build": "next build",
@@ -23,7 +23,12 @@ const tool = (label, command, args) =>
23
23
  check(label, () => execFileSync(command, args, { stdio: "pipe" }))
24
24
 
25
25
  tool("git available", "git", ["--version"])
26
- tool("node >= 20", "node", ["--version"])
26
+ check("node >= 22 (the fixtures pin it in .nvmrc and engines)", () => {
27
+ const version = execFileSync("node", ["--version"], { encoding: "utf8" }).trim()
28
+ if (Number(version.replace(/^v/, "").split(".")[0]) < 22) {
29
+ throw new Error(`${version} on PATH; the gates need Node 22 (nvm use 22 / fnm use 22)`)
30
+ }
31
+ })
27
32
  tool("pnpm available", "pnpm", ["--version"])
28
33
  tool("claude CLI available (the coder seat)", "claude", ["--version"])
29
34
  check("llm4ts CLI available", () => execFileSync("llm4ts", ["--version"], { stdio: "pipe" }))
@@ -14,6 +14,7 @@ import { budget, capped } from "@llm4ts/flow/Context";
14
14
  import { Decisions, parseDecisions } from "@llm4ts/flow/Decisions";
15
15
  import { navigationOrder, parseDomains } from "@llm4ts/flow/Domains";
16
16
  import { FlowEvents } from "@llm4ts/flow/FlowEvents";
17
+ import { nodePreflight } from "@llm4ts/flow/NodePreflight";
17
18
  import { openApiFor, openApiForFeature, parsePageSpec, renderPageSpec } from "@llm4ts/flow/PageSpec";
18
19
  import { loadPatternCards, matchingPatternCards } from "@llm4ts/flow/Patterns";
19
20
  import { Task } from "@llm4ts/flow/Plan";
@@ -554,6 +555,8 @@ export const setupConversion = Effect.fn("convert.setup")(function* (context, in
554
555
  };
555
556
  const legacy = yield* makeNodeWorkspace(legacyDir, workspaceLimitsFromEnv(environment, legacySourceWorkspaceLimits));
556
557
  const target = yield* makeNodeWorkspace(input.workDir);
558
+ // The pack's gates run on the node on PATH: check it against the target's pin first.
559
+ yield* nodePreflight(nodeProcessExecutor, nodePlainFileStore, context.events, input.workDir, environment);
557
560
  const opened = yield* stage(context.events, "pack", openPack({
558
561
  environment: {
559
562
  ...environment,
@@ -2,6 +2,9 @@
2
2
  "name": "meridian-spa",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
+ "engines": {
6
+ "node": ">=22"
7
+ },
5
8
  "scripts": {
6
9
  "test": "node --test tests/*.test.js",
7
10
  "build": "next build",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "2.24.0",
3
+ "version": "2.25.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.24.0",
56
- "@llm4ts/flow": "2.24.0",
57
- "@llm4ts/runner": "2.24.0"
55
+ "@llm4ts/core": "2.25.0",
56
+ "@llm4ts/flow": "2.25.0",
57
+ "@llm4ts/runner": "2.25.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "effect": "4.0.0"