@llm4ts/shell 2.1.0 → 2.2.1

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.
Files changed (32) hide show
  1. package/dist/Cli.d.ts +1 -1
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +32 -0
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/Refine.d.ts +44 -0
  6. package/dist/Refine.d.ts.map +1 -0
  7. package/dist/Refine.js +362 -0
  8. package/dist/Refine.js.map +1 -0
  9. package/flows/lib/modernize-extract.js +226 -0
  10. package/flows/modernize-extract.js +13 -173
  11. package/flows/modernize-implement.js +28 -2
  12. package/flows/modernize-pack-check.js +7 -1
  13. package/flows/modernize-pack-upgrade.js +246 -0
  14. package/flows/modernize-refine.js +389 -0
  15. package/flows/modernize-seed.js +50 -2
  16. package/flows/modernize-verify.js +12 -5
  17. package/kits/j2ee-nextjs/README.md +5 -4
  18. package/kits/j2ee-nextjs/fixtures/demo-bank/RUNBOOK.md +43 -0
  19. package/kits/j2ee-nextjs/fixtures/demo-bank/legacy-j2ee/PAGES.md +26 -0
  20. package/kits/j2ee-nextjs/flows/convert-all.js +56 -20
  21. package/kits/j2ee-nextjs/flows/convert-feature.js +48 -0
  22. package/kits/j2ee-nextjs/flows/lib/convert.js +292 -40
  23. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/pack.md +16 -0
  24. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/consolidate.md +10 -0
  25. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/plan.md +24 -16
  26. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/refine-propose.md +16 -0
  27. package/kits/mainframe-java/packs/cobol-springboot/pack.md +5 -0
  28. package/kits/mainframe-java/packs/cobol-springboot/prompts/consolidate.md +8 -0
  29. package/kits/mainframe-java/packs/cobol-springboot/prompts/refine-propose.md +10 -0
  30. package/package.json +5 -4
  31. package/src/Cli.ts +57 -0
  32. package/src/Refine.ts +504 -0
@@ -27,6 +27,7 @@ import { packageVersion } from "@llm4ts/flow/Package";
27
27
  import { Provenance, makeProvenanceStore } from "@llm4ts/flow/Provenance";
28
28
  import { matchingFiles } from "@llm4ts/flow/SpecChecks";
29
29
  import { requireApproval } from "@llm4ts/flow/Approval";
30
+ import { Decisions, filterFeature, parseDecisions } from "@llm4ts/flow/Decisions";
30
31
  const ModDir = "docs/modernization";
31
32
  const skipped = new Set([".git", "target", "node_modules", "dist"]);
32
33
  const Seats = Schema.Record(Schema.String, Schema.String);
@@ -84,7 +85,20 @@ const program = Effect.gen(function* () {
84
85
  }));
85
86
  const pack = opened.pack;
86
87
  const specPackRoot = join(legacyRepo, ModDir);
87
- yield* stage(context.events, "approval", requireApproval(files, join(specPackRoot, "README.md")));
88
+ // The README always; each refinement overlay when it exists (ADR 0015):
89
+ // a pack refined after its gate passed is seeded only as approved.
90
+ yield* stage(context.events, "approval", Effect.gen(function* () {
91
+ yield* requireApproval(files, join(specPackRoot, "README.md"));
92
+ for (const overlay of ["decisions.md", "domains.md"]) {
93
+ if ((yield* files.read(join(specPackRoot, overlay))) !== undefined) {
94
+ yield* requireApproval(files, join(specPackRoot, overlay));
95
+ }
96
+ }
97
+ }));
98
+ const decisionsText = yield* files.read(join(specPackRoot, "decisions.md"));
99
+ const decisions = decisionsText === undefined
100
+ ? Decisions.empty()
101
+ : yield* parseDecisions(decisionsText, `${ModDir}/decisions.md`);
88
102
  yield* stage(context.events, "scaffold", Effect.gen(function* () {
89
103
  const existing = (yield* target.discover().pipe(Effect.orElseSucceed(() => []))).filter((path) => !path.startsWith(".git/"));
90
104
  if (existing.length > 0) {
@@ -116,8 +130,42 @@ const program = Effect.gen(function* () {
116
130
  "check LLM4TS_LEGACY_REPO and that extraction wrote its spec pack"
117
131
  });
118
132
  }
133
+ // The projection (ADR 0015): a feature file reaches the target
134
+ // with only its surviving scenarios, so the coder never sees a
135
+ // scenario it must not encode; a program disposed as a whole
136
+ // ships no feature file at all. Specs are copied unchanged, with
137
+ // the overlays beside them for the judges.
119
138
  const features = yield* copyTree(legacy, target, `${ModDir}/features`, pack.featuresDir);
120
- for (const index of ["traceability.md", "mapping.md", "rules.txt"]) {
139
+ let projected = 0;
140
+ if (!decisions.isEmpty) {
141
+ const featurePaths = yield* target
142
+ .discover(`${pack.featuresDir}/**`)
143
+ .pipe(Effect.orElseSucceed(() => []));
144
+ for (const path of featurePaths) {
145
+ const stem = (path.split("/").at(-1) ?? "").replace(/\.feature$/, "").toLowerCase();
146
+ const program = [...decisions.programs, ...decisions.scenarios]
147
+ .map((entry) => entry.program)
148
+ .find((name) => name.toLowerCase() === stem);
149
+ if (program === undefined) {
150
+ continue;
151
+ }
152
+ projected += 1;
153
+ if (decisions.programDecision(program) !== undefined) {
154
+ yield* target.write(path, "");
155
+ continue;
156
+ }
157
+ const text = yield* target.read(path);
158
+ yield* target.write(path, filterFeature(text, decisions.disposedScenarios(program)));
159
+ }
160
+ yield* context.events.publish(Info.make({ message: `projected decisions onto ${projected} feature file(s)` }));
161
+ }
162
+ for (const index of [
163
+ "traceability.md",
164
+ "mapping.md",
165
+ "rules.txt",
166
+ "decisions.md",
167
+ "domains.md"
168
+ ]) {
121
169
  yield* copyFile(legacy, target, `${ModDir}/${index}`, join(pack.specsDir, index));
122
170
  }
123
171
  return specs + features;
@@ -229,7 +229,7 @@ const triagePrompt = (pack, failing, specText) => {
229
229
  /** The spec'd programs: top-level `<NAME>.md` files under the specs dir, indexes aside. */
230
230
  const specPrograms = Effect.fn("modernize-verify.specPrograms")(function* (target, specsDir) {
231
231
  const paths = yield* matchingFiles(target, `^${specsDir}/[^/]+\\.md$`).pipe(Effect.orElseSucceed(() => []));
232
- const excluded = new Set(["traceability", "mapping", "README"]);
232
+ const excluded = new Set(["traceability", "mapping", "README", "decisions", "domains"]);
233
233
  return paths
234
234
  .map((path) => (path.split("/").at(-1) ?? path).replace(/\.md$/, ""))
235
235
  .filter((name) => !excluded.has(name))
@@ -268,10 +268,17 @@ const program = Effect.gen(function* () {
268
268
  yield* context.events.publish(Info.make({ message: "clean-room wall: no legacy source in the target workspace" }));
269
269
  }));
270
270
  const rulesText = (yield* files.read(join(input.workDir, pack.specsDir, "rules.txt"))) ?? "";
271
- const universe = rulesText
272
- .split(/\r?\n/)
273
- .map((line) => line.trim())
274
- .filter((line) => line.length > 0);
271
+ // Everything above `# waived` is the universe; the units below it were
272
+ // waived by a decision (ADR 0015) and are neither expected nor flagged.
273
+ const rulesLines = rulesText.split(/\r?\n/).map((line) => line.trim());
274
+ const waivedAt = rulesLines.indexOf("# waived");
275
+ const universe = (waivedAt < 0 ? rulesLines : rulesLines.slice(0, waivedAt)).filter((line) => line.length > 0);
276
+ const waived = waivedAt < 0 ? [] : rulesLines.slice(waivedAt + 1).filter((l) => l.length > 0);
277
+ if (waived.length > 0) {
278
+ yield* context.events.publish(Info.make({
279
+ message: `${waived.length} rule(s) waived by decision are left out of the universe`
280
+ }));
281
+ }
275
282
  if (universe.length === 0) {
276
283
  yield* context.events.publish(Info.make({
277
284
  message: "no rules.txt in the seeded spec pack — the report will use the vectors' own " +
@@ -15,10 +15,11 @@ target) the [workshop runbook](fixtures/demo-bank/RUNBOOK.md) rehearses on.
15
15
  | [`jsp-nextjs`](packs/jsp-nextjs/pack.md) | JSP/Java → Next.js SPA | `nextjs-spa` | no |
16
16
  | [`jsp-bff-nextjs`](packs/jsp-bff-nextjs/pack.md) | JSP/Java → Spring BFF + Next.js | `spring-bff` | no |
17
17
 
18
- | Flow | What it does |
19
- | -------------- | ---------------------------------------------------------------------- |
20
- | `convert-page` | Convert ONE extracted page into the Next.js target on its own branch |
21
- | `convert-all` | Walk the survey inventory in wave order, one branch per page, a report |
18
+ | Flow | What it does |
19
+ | ----------------- | ---------------------------------------------------------------------------------------- |
20
+ | `convert-page` | Convert ONE extracted page into the Next.js target on its own branch |
21
+ | `convert-feature` | Convert ONE domain feature of the approved `domains.md`: one branch, one merged contract |
22
+ | `convert-all` | Walk the approved domain map (features) or the survey inventory (pages), one branch each |
22
23
 
23
24
  ```sh
24
25
  llm4ts run modernize-pack-check --pack j2ee-nextjs-spa --repo /path/to/legacy-estate
@@ -119,6 +119,49 @@ estate once every wave is in.
119
119
  LLM4TS_WAVE=wave-2 llm4ts run modernize-extract --repo ~/demo/legacy-j2ee
120
120
  ```
121
121
 
122
+ ### Refine — the pack becomes what the client wants built (optional beat)
123
+
124
+ Extraction says what the legacy does. Refinement (ADR 0015) says what the
125
+ target should become, in two files the audience can read on screen:
126
+
127
+ ```bash
128
+ llm4ts refine --repo ~/demo/legacy-j2ee --target ~/demo/nextjs
129
+ ```
130
+
131
+ Three marks, then run:
132
+
133
+ 1. **Mark programs** — `promoQ3`, `oldTransfer`, `testHarness`: `drop`
134
+ ("dead, nothing links here"); `login`: `?` with the note "the target has
135
+ an AuthProvider". The proposal reads `~/demo/nextjs` read-only and comes
136
+ back with `provided — src/auth/AuthProvider.tsx`, or an open point if it
137
+ could not find the proof. Say that `defer` is never proposed by the model.
138
+ 2. **Deepen a program** — `accountOverview` with the focus "the date-range
139
+ filter on movements is missing; check AccountOverviewServlet". Watch the
140
+ analyst revise the spec (own commit, `deepen accountOverview`), the judge
141
+ score the focus, and the mark stamp `[done <commit>]`.
142
+ 3. **Run modernize-refine** — the map lands at
143
+ `docs/modernization/domains.md`: `beneficiaryList` + `beneficiaryEdit`
144
+ become "Beneficiary maintenance" (they share the `/beneficiary` form
145
+ target), the three transfer steps "Wire transfer", header/nav/footer the
146
+ shell; the filler pages are folded only if the model proposed it with
147
+ evidence. `plan.md` is regenerated per feature and `rules.txt` gains a
148
+ `# waived` section listing the dropped pages' units. Answer any open
149
+ point in the menu, rerun, then **Approve the overlays** — and point out
150
+ that the README went back to `- [ ] Approved` the moment refine wrote
151
+ anything.
152
+
153
+ Off stage the same beat is: edit `decisions.md` by hand (its header explains
154
+ the vocabulary) and `LLM4TS_TARGET_REPO=~/demo/nextjs llm4ts run
155
+ modernize-refine --repo ~/demo/legacy-j2ee`. Skip the beat entirely and the
156
+ pipeline behaves exactly as before.
157
+
158
+ Once `domains.md` is approved, Act 2 may convert **one feature instead of
159
+ two pages**: `llm4ts run convert-feature --repo ~/demo/nextjs
160
+ beneficiary-maintenance` lands `beneficiaryList` and `beneficiaryEdit` on one
161
+ `convert/beneficiary-maintenance` branch behind one merged contract, and
162
+ `convert-all` walks the features rather than the pages. Rehearse the timing
163
+ before choosing it on stage: it is the two pages' cost in one run.
164
+
122
165
  Extraction runs three pages at once (`LLM4TS_EXTRACT_CONCURRENCY=3` from Act
123
166
  0): the pages of a wave are independent, each lands in its own commit holding
124
167
  only its four files, and the log interleaves — say so before it starts, then
@@ -65,3 +65,29 @@ anti-corruption renaming in a Page Spec has something to bite on.
65
65
 
66
66
  All data is fictional: EUR accounts, fake IBAN-like numbers in the form
67
67
  `IT00 DEMO 0000 ...`, customer "MARIO BIANCHI" / customer id `CUST0042`.
68
+
69
+ ## Refinement answer key (ADR 0015)
70
+
71
+ What `modernize-refine` should produce on this estate once the three dead
72
+ pages are marked `drop` and `login` resolves to `provided` by the target's
73
+ AuthProvider:
74
+
75
+ | Domain feature (deterministic cluster) | Programs | Why they cluster |
76
+ | ------------------------------------------------------------ | --------------------------------------------------------- | --------------------------------------------- |
77
+ | Portal shell | `header.jsp`, `nav.jsp`, `footer.jsp` | fragments: targets of `jsp-include` edges |
78
+ | Account overview | `accountOverview.jsp` | its ajax target names no other page |
79
+ | Beneficiary maintenance | `beneficiaryList.jsp`, `beneficiaryEdit.jsp` | both post to `/beneficiary` (`jsp-form-action`) |
80
+ | Wire transfer | `transferStep1.jsp`, `transferStep2.jsp`, `transferConfirm.jsp` | all post to `/transfer` |
81
+ | one singleton each (fold into the shell only by proposal) | `dashboard.jsp`, `settings.jsp`, `profile.jsp`, `messages.jsp`, `help.jsp` | no shared form or ajax target |
82
+
83
+ Waived coverage units after the drops: every `url-pattern`, form action, and
84
+ ajax url captured only from `oldTransfer.jsp`, `promoQ3.jsp`, and
85
+ `testHarness.jsp`; `login.jsp`'s form action once it is `provided`.
86
+
87
+ Feature contracts `convert-feature` writes once the map above is approved:
88
+ `contracts/beneficiary-maintenance.openapi.yaml` unions `GET /beneficiary`
89
+ (list, both pages), `GET /beneficiary?action=edit` and `POST /beneficiary`
90
+ (edit) over the shared `Beneficiary` DTO; `contracts/wire-transfer.openapi.yaml`
91
+ unions the three `POST /transfer` steps — same path, one operation per
92
+ `step`, so the pages must agree on the `TransferDraft` DTO or the map gets a
93
+ conflict open point.
@@ -1,4 +1,4 @@
1
- // Convert the whole legacy estate: walk the survey inventory in wave order, one branch per page, progress board, estimated-cost migration report.
1
+ // Convert the whole legacy estate: walk the approved domain map (one branch per feature) or the survey inventory (one branch per page) in wave order, progress board, estimated-cost migration report.
2
2
  //
3
3
  // Runs rooted at the TARGET repository (`--repo <nextjs>`), with
4
4
  // LLM4TS_LEGACY_REPO pointing at the extracted legacy repository:
@@ -24,7 +24,7 @@ import { AdoConfig, makeAzureDevOpsTool } from "@llm4ts/flow/AzureDevOpsTool";
24
24
  import { BoardItem, composeBoardSync, makeAdoBoardSync, makeLocalBoardSync } from "@llm4ts/flow/BoardSync";
25
25
  import { describeFlowError } from "@llm4ts/flow/FlowError";
26
26
  import { Info, asReadOnly, coderFromEnv, nodePlainFileStore, nodeProcessExecutor, resolveFlowInput, runFlowMain, runNode, stage } from "@llm4ts/runner";
27
- import { conversionInventory, convertPage, migrationReport, setupConversion } from "./lib/convert.js";
27
+ import { conversionInventory, convertFeature, convertPage, featureInventory, migrationReport, setupConversion } from "./lib/convert.js";
28
28
  const program = Effect.gen(function* () {
29
29
  const input = yield* resolveFlowInput("Convert the legacy estate into the destination SPA");
30
30
  const coder = coderFromEnv(process.env);
@@ -57,39 +57,75 @@ const program = Effect.gen(function* () {
57
57
  yield* context.events.publish(Info.make({ message: `ADO board mirror enabled: ${orgUrl}/${project}` }));
58
58
  }
59
59
  const board = composeBoardSync(boards);
60
- const inventory = yield* stage(context.events, "inventory", conversionInventory(files, deps.legacy, deps.legacyDir, deps.pack));
61
- if (inventory.length === 0) {
60
+ // ADR 0012 addendum: an approved domain map makes the feature the
61
+ // unit of delivery; without one the walk is per page as before.
62
+ const features = yield* stage(context.events, "inventory", featureInventory(files, deps.legacy, deps.legacyDir, deps.pack));
63
+ let items;
64
+ if (features !== undefined) {
65
+ yield* context.events.publish(Info.make({
66
+ message: `approved domain map: converting ${features.length} feature(s), one branch each`
67
+ }));
68
+ items = features.map((entry) => ({
69
+ id: entry.feature.id,
70
+ title: entry.feature.name,
71
+ ...(entry.wave === undefined ? {} : { wave: entry.wave }),
72
+ detail: `pages: ${entry.feature.programs.join(", ")}`,
73
+ ...(entry.disposed ? { skip: "every page disposed by decision" } : {}),
74
+ convert: convertFeature(deps, entry.feature.id)
75
+ }));
76
+ }
77
+ else {
78
+ const inventory = yield* conversionInventory(files, deps.legacy, deps.legacyDir, deps.pack);
79
+ items = yield* Effect.forEach(inventory, ({ page, wave, disposition }) => Effect.gen(function* () {
80
+ const specPath = join(deps.legacyDir, deps.pack.specsDir, `${page}.md`);
81
+ const missing = (yield* files.read(specPath)) === undefined;
82
+ return {
83
+ id: page,
84
+ title: page,
85
+ ...(wave === undefined ? {} : { wave }),
86
+ // A page the decisions overlay disposed of as a whole (ADR 0015)
87
+ // is listed with its disposition, like a page triaged dead.
88
+ ...(disposition !== undefined
89
+ ? { skip: `${disposition} by decision` }
90
+ : missing
91
+ ? { skip: "no extracted spec" }
92
+ : {}),
93
+ convert: convertPage(deps, page)
94
+ };
95
+ }));
96
+ }
97
+ if (items.length === 0) {
62
98
  yield* context.events.publish(Info.make({ message: "inventory is empty — extract the legacy estate first" }));
63
99
  return;
64
100
  }
65
101
  // The whole estate lands on the board as planned up front — the
66
102
  // breadth view exists from minute one.
67
- yield* stage(context.events, "board", board.plan(inventory.map(({ page, wave }) => BoardItem.make({
68
- id: page,
69
- title: page,
103
+ yield* stage(context.events, "board", board.plan(items.map((item) => BoardItem.make({
104
+ id: item.id,
105
+ title: item.title,
70
106
  status: "planned",
71
- ...(wave === undefined ? {} : { wave })
107
+ ...(item.wave === undefined ? {} : { wave: item.wave }),
108
+ ...(item.detail === undefined ? {} : { detail: item.detail })
72
109
  }))));
73
110
  const baseBranch = yield* context.git.currentBranch;
74
111
  const failFast = process.env.LLM4TS_FAIL_FAST === "1";
75
- for (const { page } of inventory) {
112
+ for (const item of items) {
76
113
  const snapshot = yield* board.snapshot;
77
- const known = snapshot.items.find((item) => item.id === page);
114
+ const known = snapshot.items.find((candidate) => candidate.id === item.id);
78
115
  if (known !== undefined && known.status !== "planned" && known.status !== "failed") {
79
- yield* context.events.publish(Info.make({ message: `resume: ${page} is already ${known.status} — skipping` }));
116
+ yield* context.events.publish(Info.make({ message: `resume: ${item.id} is already ${known.status} — skipping` }));
80
117
  continue;
81
118
  }
82
- const specPath = join(deps.legacyDir, deps.pack.specsDir, `${page}.md`);
83
- if ((yield* files.read(specPath)) === undefined) {
84
- yield* board.skip(page, "no extracted spec");
119
+ if (item.skip !== undefined) {
120
+ yield* board.skip(item.id, item.skip);
85
121
  continue;
86
122
  }
87
123
  const checkpoint = yield* context.git.checkpoint;
88
- yield* board.start(page);
89
- const result = yield* Effect.result(convertPage(deps, page));
124
+ yield* board.start(item.id);
125
+ const result = yield* Effect.result(item.convert);
90
126
  if (result._tag === "Success") {
91
127
  const outcome = result.success;
92
- yield* board.complete(page, {
128
+ yield* board.complete(item.id, {
93
129
  branch: outcome.branch,
94
130
  reportPath: outcome.reportPath,
95
131
  ...(outcome.estimatedTokens === undefined
@@ -103,15 +139,15 @@ const program = Effect.gen(function* () {
103
139
  }
104
140
  else {
105
141
  const reason = describeFlowError(result.failure);
106
- // A stuck page must not sink the walk: reset the working tree,
142
+ // A stuck unit must not sink the walk: reset the working tree,
107
143
  // mark the failure, keep going (LLM4TS_FAIL_FAST=1 to stop).
108
144
  yield* context.git.rollback(checkpoint);
109
145
  yield* context.git.checkout(baseBranch);
110
- yield* board.fail(page, reason);
146
+ yield* board.fail(item.id, reason);
111
147
  if (failFast) {
112
148
  return yield* Effect.fromResult(result);
113
149
  }
114
- yield* context.events.publish(Info.make({ message: `page ${page} failed — continuing: ${reason}` }));
150
+ yield* context.events.publish(Info.make({ message: `${item.id} failed — continuing: ${reason}` }));
115
151
  }
116
152
  }
117
153
  const finalBoard = yield* board.snapshot;
@@ -0,0 +1,48 @@
1
+ // Convert ONE domain feature of the approved domains.md into the destination Next.js SPA: one branch, one merged contract, the port then each page in navigation order.
2
+ //
3
+ // Runs rooted at the TARGET repository (`--repo <nextjs>`), with
4
+ // LLM4TS_LEGACY_REPO pointing at the refined legacy repository whose
5
+ // docs/modernization/domains.md is approved. The task text is the feature id:
6
+ //
7
+ // LLM4TS_LEGACY_REPO=~/estates/demo-bank-legacy \
8
+ // llm4ts run convert-feature --repo ~/estates/demo-bank-nextjs beneficiary-maintenance
9
+ //
10
+ // ADR 0012 addendum: the unit of delivery is the domain feature — its
11
+ // surviving pages share `contracts/<feature>.openapi.yaml` (the deterministic
12
+ // union of their API sections; conflicts are open points in domains.md, never
13
+ // silent merges), one port under src/services/<feature>/, page components
14
+ // and tests per page. Gates and judge as convert-page, plus the feature
15
+ // judged against its contract of record. Branch `convert/<feature>`, no PR.
16
+ import * as Effect from "effect/Effect";
17
+ import { FlowAborted, Info, asReadOnly, coderFromEnv, resolveFlowInput, runFlowMain, runNode } from "@llm4ts/runner";
18
+ import { convertFeature, setupConversion } from "./lib/convert.js";
19
+ const program = Effect.gen(function* () {
20
+ const input = yield* resolveFlowInput("Convert one domain feature into the destination SPA");
21
+ const feature = input.prompt.trim().split(/\s+/)[0] ?? "";
22
+ const coder = coderFromEnv(process.env);
23
+ yield* runNode({
24
+ workDir: input.workDir,
25
+ workspace: input.workspace,
26
+ userPrompt: input.prompt,
27
+ coder,
28
+ reasoning: asReadOnly(coder),
29
+ reviewers: [asReadOnly(coder)],
30
+ environment: process.env
31
+ }, (context) => Effect.gen(function* () {
32
+ if (feature.length === 0) {
33
+ return yield* FlowAborted.make({
34
+ message: "pass the domain feature id to convert, e.g.: llm4ts run convert-feature beneficiary-maintenance"
35
+ });
36
+ }
37
+ const deps = yield* setupConversion(context, input, process.env, import.meta.dirname);
38
+ const outcome = yield* convertFeature(deps, feature);
39
+ yield* context.events.publish(Info.make({
40
+ message: `converted feature ${outcome.page} on branch ${outcome.branch} — ` +
41
+ `report at ${outcome.reportPath}` +
42
+ (outcome.estimatedTokens === undefined
43
+ ? ""
44
+ : ` (~${outcome.estimatedTokens} tokens, ESTIMATED)`)
45
+ }));
46
+ }));
47
+ });
48
+ runFlowMain(program);