@llm4ts/shell 0.5.0 → 0.6.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 (168) hide show
  1. package/dist/Cli.d.ts +2 -2
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +8 -4
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/FlowCatalog.d.ts.map +1 -1
  6. package/dist/FlowCatalog.js +6 -2
  7. package/dist/FlowCatalog.js.map +1 -1
  8. package/flows/fixtures/scaffolds/kafka-streams-service/README.md +12 -0
  9. package/flows/fixtures/scaffolds/kafka-streams-service/pom.xml +49 -0
  10. package/flows/fixtures/scaffolds/kafka-streams-service/scripts/replay.sh +10 -0
  11. package/flows/fixtures/scaffolds/kafka-streams-service/src/main/java/com/meridian/scaffold/Application.java +29 -0
  12. package/flows/fixtures/scaffolds/kafka-streams-service/src/test/java/com/meridian/scaffold/ApplicationTests.java +21 -0
  13. package/flows/fixtures/scaffolds/kafka-streams-service/src/test/resources/features/.gitkeep +0 -0
  14. package/flows/fixtures/scaffolds/nextjs-spa/README.md +6 -0
  15. package/flows/fixtures/scaffolds/nextjs-spa/app/layout.js +11 -0
  16. package/flows/fixtures/scaffolds/nextjs-spa/app/page.js +7 -0
  17. package/flows/fixtures/scaffolds/nextjs-spa/docs/specs/.gitkeep +0 -0
  18. package/flows/fixtures/scaffolds/nextjs-spa/features/.gitkeep +0 -0
  19. package/flows/fixtures/scaffolds/nextjs-spa/next.config.js +7 -0
  20. package/flows/fixtures/scaffolds/nextjs-spa/package.json +15 -0
  21. package/flows/fixtures/scaffolds/nextjs-spa/tests/smoke.test.js +6 -0
  22. package/flows/fixtures/scaffolds/spring-bff/README.md +9 -0
  23. package/flows/fixtures/scaffolds/spring-bff/frontend/app/layout.js +11 -0
  24. package/flows/fixtures/scaffolds/spring-bff/frontend/app/page.js +7 -0
  25. package/flows/fixtures/scaffolds/spring-bff/frontend/next.config.js +7 -0
  26. package/flows/fixtures/scaffolds/spring-bff/frontend/package.json +15 -0
  27. package/flows/fixtures/scaffolds/spring-bff/frontend/tests/smoke.test.js +6 -0
  28. package/flows/fixtures/scaffolds/spring-bff/pom.xml +57 -0
  29. package/flows/fixtures/scaffolds/spring-bff/scripts/test.sh +6 -0
  30. package/flows/fixtures/scaffolds/spring-bff/src/main/java/com/meridian/bff/Application.java +12 -0
  31. package/flows/fixtures/scaffolds/spring-bff/src/main/resources/application.yaml +12 -0
  32. package/flows/fixtures/scaffolds/spring-bff/src/test/java/com/meridian/bff/ApplicationTests.java +12 -0
  33. package/flows/fixtures/scaffolds/spring-bff/src/test/resources/features/.gitkeep +0 -0
  34. package/flows/fixtures/scaffolds/spring-boot-service/README.md +7 -0
  35. package/flows/fixtures/scaffolds/spring-boot-service/pom.xml +53 -0
  36. package/flows/fixtures/scaffolds/spring-boot-service/scripts/replay.sh +9 -0
  37. package/flows/fixtures/scaffolds/spring-boot-service/src/main/java/com/meridian/scaffold/Application.java +12 -0
  38. package/flows/fixtures/scaffolds/spring-boot-service/src/main/resources/application.yaml +12 -0
  39. package/flows/fixtures/scaffolds/spring-boot-service/src/test/java/com/meridian/scaffold/ApplicationTests.java +12 -0
  40. package/flows/fixtures/scaffolds/spring-boot-service/src/test/resources/features/.gitkeep +0 -0
  41. package/flows/implement.js +29 -0
  42. package/flows/issue-pr.js +71 -0
  43. package/flows/judge-suite.js +48 -0
  44. package/flows/local.js +40 -0
  45. package/flows/modernize-bench.js +263 -0
  46. package/flows/modernize-extract.js +454 -0
  47. package/flows/modernize-implement.js +249 -0
  48. package/flows/modernize-review.js +237 -0
  49. package/flows/modernize-seed.js +189 -0
  50. package/flows/modernize-survey.js +0 -0
  51. package/flows/modernize-verify.js +435 -0
  52. package/flows/packs/ace-integration/lessons.md +6 -0
  53. package/flows/packs/ace-integration/pack.md +29 -0
  54. package/flows/packs/ace-integration/prompts/analysis.md +21 -0
  55. package/flows/packs/ace-integration/prompts/bdd.md +13 -0
  56. package/flows/packs/ace-integration/prompts/implement.md +17 -0
  57. package/flows/packs/ace-integration/prompts/plan.md +12 -0
  58. package/flows/packs/ace-integration/prompts/review.md +17 -0
  59. package/flows/packs/ace-integration/prompts/spec.md +29 -0
  60. package/flows/packs/ace-integration/reviewers/routing-fidelity.md +19 -0
  61. package/flows/packs/ace-integration/reviewers/traceability.md +13 -0
  62. package/flows/packs/ace-kafka/lessons.md +6 -0
  63. package/flows/packs/ace-kafka/pack.md +36 -0
  64. package/flows/packs/ace-kafka/prompts/analysis.md +21 -0
  65. package/flows/packs/ace-kafka/prompts/bdd.md +13 -0
  66. package/flows/packs/ace-kafka/prompts/implement.md +34 -0
  67. package/flows/packs/ace-kafka/prompts/plan.md +12 -0
  68. package/flows/packs/ace-kafka/prompts/review.md +17 -0
  69. package/flows/packs/ace-kafka/prompts/spec.md +31 -0
  70. package/flows/packs/ace-kafka/prompts/vectors.md +21 -0
  71. package/flows/packs/ace-kafka/reviewers/event-contracts.md +9 -0
  72. package/flows/packs/ace-kafka/reviewers/routing-fidelity.md +19 -0
  73. package/flows/packs/ace-kafka/reviewers/traceability.md +13 -0
  74. package/flows/packs/cobol-kafka/lessons.md +3 -0
  75. package/flows/packs/cobol-kafka/pack.md +51 -0
  76. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-001-record-to-event.md +7 -0
  77. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-002-job-step-topology.md +6 -0
  78. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-003-checkpoint-idempotence.md +7 -0
  79. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-004-eod-totals-window.md +7 -0
  80. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-005-reject-topic.md +6 -0
  81. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-006-keyed-state-store.md +7 -0
  82. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-007-outbox.md +7 -0
  83. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-008-partition-key.md +7 -0
  84. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-009-batch-window-cutoff.md +7 -0
  85. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-010-replay-backfill.md +7 -0
  86. package/flows/packs/cobol-kafka/prompts/analysis.md +23 -0
  87. package/flows/packs/cobol-kafka/prompts/bdd.md +15 -0
  88. package/flows/packs/cobol-kafka/prompts/implement.md +47 -0
  89. package/flows/packs/cobol-kafka/prompts/plan.md +13 -0
  90. package/flows/packs/cobol-kafka/prompts/review.md +17 -0
  91. package/flows/packs/cobol-kafka/prompts/spec.md +28 -0
  92. package/flows/packs/cobol-kafka/prompts/vectors.md +21 -0
  93. package/flows/packs/cobol-kafka/reviewers/cobol-fidelity.md +16 -0
  94. package/flows/packs/cobol-kafka/reviewers/pattern-conformance.md +10 -0
  95. package/flows/packs/cobol-kafka/reviewers/traceability.md +13 -0
  96. package/flows/packs/cobol-springboot/lessons.md +3 -0
  97. package/flows/packs/cobol-springboot/pack.md +51 -0
  98. package/flows/packs/cobol-springboot/prompts/analysis.md +23 -0
  99. package/flows/packs/cobol-springboot/prompts/bdd.md +15 -0
  100. package/flows/packs/cobol-springboot/prompts/implement.md +31 -0
  101. package/flows/packs/cobol-springboot/prompts/plan.md +13 -0
  102. package/flows/packs/cobol-springboot/prompts/review.md +17 -0
  103. package/flows/packs/cobol-springboot/prompts/spec.md +28 -0
  104. package/flows/packs/cobol-springboot/prompts/vectors.md +22 -0
  105. package/flows/packs/cobol-springboot/reviewers/cobol-fidelity.md +16 -0
  106. package/flows/packs/cobol-springboot/reviewers/pattern-conformance.md +10 -0
  107. package/flows/packs/cobol-springboot/reviewers/traceability.md +13 -0
  108. package/flows/packs/jsp-bff-nextjs/lessons.md +6 -0
  109. package/flows/packs/jsp-bff-nextjs/pack.md +30 -0
  110. package/flows/packs/jsp-bff-nextjs/prompts/analysis.md +20 -0
  111. package/flows/packs/jsp-bff-nextjs/prompts/bdd.md +10 -0
  112. package/flows/packs/jsp-bff-nextjs/prompts/implement.md +17 -0
  113. package/flows/packs/jsp-bff-nextjs/prompts/plan.md +12 -0
  114. package/flows/packs/jsp-bff-nextjs/prompts/review.md +16 -0
  115. package/flows/packs/jsp-bff-nextjs/prompts/spec.md +27 -0
  116. package/flows/packs/jsp-bff-nextjs/reviewers/bff-boundary.md +18 -0
  117. package/flows/packs/jsp-bff-nextjs/reviewers/traceability.md +13 -0
  118. package/flows/packs/jsp-nextjs/lessons.md +6 -0
  119. package/flows/packs/jsp-nextjs/pack.md +29 -0
  120. package/flows/packs/jsp-nextjs/prompts/analysis.md +21 -0
  121. package/flows/packs/jsp-nextjs/prompts/bdd.md +12 -0
  122. package/flows/packs/jsp-nextjs/prompts/implement.md +17 -0
  123. package/flows/packs/jsp-nextjs/prompts/plan.md +14 -0
  124. package/flows/packs/jsp-nextjs/prompts/review.md +16 -0
  125. package/flows/packs/jsp-nextjs/prompts/spec.md +26 -0
  126. package/flows/packs/jsp-nextjs/reviewers/traceability.md +13 -0
  127. package/flows/packs/jsp-nextjs/reviewers/ux-fidelity.md +16 -0
  128. package/flows/patterns/PAT-COBOL-001-comp3-money.md +7 -0
  129. package/flows/patterns/PAT-COBOL-002-redefines.md +7 -0
  130. package/flows/patterns/PAT-COBOL-003-88-levels.md +7 -0
  131. package/flows/patterns/PAT-COBOL-004-perform-thru.md +7 -0
  132. package/flows/patterns/PAT-COBOL-005-file-status.md +6 -0
  133. package/flows/patterns/PAT-COBOL-006-sqlcode.md +7 -0
  134. package/flows/patterns/PAT-COBOL-007-validation-order.md +7 -0
  135. package/flows/patterns/PAT-COBOL-008-zoned-decimal.md +6 -0
  136. package/flows/patterns/PAT-COBOL-009-occurs-table.md +6 -0
  137. package/flows/patterns/PAT-COBOL-010-copybook-record.md +7 -0
  138. package/flows/patterns/PAT-COBOL-011-batch-counters.md +6 -0
  139. package/flows/patterns/PAT-COBOL-012-commit-frequency.md +7 -0
  140. package/flows/patterns/PAT-COBOL-013-abend.md +6 -0
  141. package/flows/patterns/PAT-COBOL-014-reject-file.md +6 -0
  142. package/flows/patterns/PAT-COBOL-015-accept-date.md +6 -0
  143. package/flows/patterns/PAT-COBOL-016-string-building.md +6 -0
  144. package/flows/patterns/PAT-COBOL-017-cursor-loop.md +6 -0
  145. package/flows/patterns/PAT-COBOL-018-yn-switches.md +6 -0
  146. package/flows/patterns/PAT-COBOL-019-shared-accumulators.md +6 -0
  147. package/flows/patterns/PAT-COBOL-020-tiered-fees.md +7 -0
  148. package/flows/patterns/PAT-COBOL-021-overdraft-floor.md +7 -0
  149. package/flows/patterns/PAT-COBOL-022-jcl-job-net.md +6 -0
  150. package/flows/patterns/PAT-COBOL-023-sort-step.md +6 -0
  151. package/flows/patterns/PAT-COBOL-024-fixed-width-records.md +6 -0
  152. package/flows/patterns/PAT-COBOL-025-audit-rows.md +6 -0
  153. package/flows/sdd.js +114 -0
  154. package/package.json +5 -4
  155. package/src/Cli.ts +13 -4
  156. package/src/FlowCatalog.ts +8 -2
  157. package/flows/implement.ts +0 -38
  158. package/flows/issue-pr.ts +0 -108
  159. package/flows/judge-suite.ts +0 -63
  160. package/flows/local.ts +0 -59
  161. package/flows/modernize-bench.ts +0 -362
  162. package/flows/modernize-extract.ts +0 -633
  163. package/flows/modernize-implement.ts +0 -333
  164. package/flows/modernize-review.ts +0 -343
  165. package/flows/modernize-seed.ts +0 -259
  166. package/flows/modernize-survey.ts +0 -0
  167. package/flows/modernize-verify.ts +0 -611
  168. package/flows/sdd.ts +0 -181
@@ -0,0 +1,454 @@
1
+ // Legacy modernization phase 1: reverse-engineer the estate into a judged, human-approved spec pack.
2
+ //
3
+ // Runs rooted at the LEGACY repository (`--repo <legacy>`), after modernize-survey's
4
+ // wave plan is approved. Extraction is PER PROGRAM and resumable
5
+ // (`extractProgramsResumably` skips every program whose spec already exists —
6
+ // delete `specs/<NAME>.md` to re-extract one program): each program gets a
7
+ // structured analyst call producing its four artifacts — `specs/<NAME>.md`,
8
+ // `features/<name>.feature`, `traceability/<NAME>.md`, `mapping/<NAME>.md` —
9
+ // and its own commit. The estate-wide indexes (`traceability.md`, `mapping.md`)
10
+ // are regenerated deterministically from the fragments before every gate round.
11
+ //
12
+ // The gate is layered and nothing auto-approves: deterministic `SpecChecks`
13
+ // (every legacy coverage unit must appear in the traceability matrix; features
14
+ // must be well-formed Gherkin; both indexes present), then an LLM-as-a-Judge
15
+ // pass per program against the pack's rubrics with full marks required.
16
+ // Findings feed one bounded fix round per sub-bar program plus one estate-wide
17
+ // residual turn; a still-dirty pack is committed as an explicit DRAFT and the
18
+ // flow halts for human triage. Even a clean pack only gets an unchecked
19
+ // `- [ ] Approved` marker in `docs/modernization/README.md`.
20
+ //
21
+ // Pack: LLM4TS_PACK=<dir> (default packs/cobol-springboot, resolved against the
22
+ // launch dir, then against the flow's own directory — the built-in packs).
23
+ // LLM4TS_WAVE=<name> scopes the run to one wave of the approved plan. Judge
24
+ // context is bounded by LLM4TS_JUDGE_SOURCES_LIMIT (chars).
25
+ import { join } from "node:path";
26
+ import * as Effect from "effect/Effect";
27
+ import { Sample } from "@llm4ts/core/eval/Eval";
28
+ import { judge } from "@llm4ts/core/eval/Judge";
29
+ import { FlowAborted, FlowLlmError } from "@llm4ts/flow/FlowError";
30
+ import { Info } from "@llm4ts/flow/FlowEvents";
31
+ import { makeChat } from "@llm4ts/flow/Chat";
32
+ import { loadPatternCards, matchingPatternCards } from "@llm4ts/flow/Patterns";
33
+ import { stage } from "@llm4ts/flow/PlanExecution";
34
+ import { defaultPlanInstructions, planFrom } from "@llm4ts/flow/Planner";
35
+ import { ReviewIssue, ReviewResult, mergeReviewResults } from "@llm4ts/flow/Review";
36
+ import { cachedReview } from "@llm4ts/flow/ReviewCache";
37
+ import { coverage, coverageUnits, features, matchingFiles } from "@llm4ts/flow/SpecChecks";
38
+ import { withDraftApproval, requireApproval } from "@llm4ts/modernize/Approval";
39
+ import { ProgramArtifacts, ProgramUnit, extractProgramsResumably } from "@llm4ts/modernize/Artifacts";
40
+ import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors";
41
+ import { resolveFlowInput } from "@llm4ts/runner/FlowArgs";
42
+ import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner";
43
+ import { reviewFingerprint } from "@llm4ts/runner/ReviewFingerprint";
44
+ import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore";
45
+ import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace";
46
+ import { loadUniversalPatternCards, openPack } from "@llm4ts/runner/Packs";
47
+ const ModDir = "docs/modernization";
48
+ const MaxRounds = 3;
49
+ const judgeSourcesLimit = () => {
50
+ const raw = Number.parseInt(process.env.LLM4TS_JUDGE_SOURCES_LIMIT ?? "", 10);
51
+ return Number.isFinite(raw) && raw > 0 ? raw : 400_000;
52
+ };
53
+ /** Past the limit keep head + tail so entry points and trailing rules stay visible. */
54
+ const capText = (text, limit) => {
55
+ if (text.length <= limit) {
56
+ return text;
57
+ }
58
+ const head = Math.floor((limit * 3) / 4);
59
+ return `${text.slice(0, head)}\n\n… [truncated] …\n\n${text.slice(text.length - (limit - head))}`;
60
+ };
61
+ /** `cobol/ACCTXFR.cbl` → `ACCTXFR`: the program name keying every per-program artifact. */
62
+ const programName = (relativePath) => {
63
+ const base = relativePath.slice(relativePath.lastIndexOf("/") + 1);
64
+ const dot = base.lastIndexOf(".");
65
+ return dot > 0 ? base.slice(0, dot) : base;
66
+ };
67
+ /** The `- PROG` entries of `## Wave: <name>` in the survey's wave plan. */
68
+ const wavePrograms = (planText, wave) => {
69
+ const lines = planText.split(/\r?\n/);
70
+ const start = lines.findIndex((line) => line.trim() === `## Wave: ${wave}`);
71
+ if (start < 0) {
72
+ return [];
73
+ }
74
+ const section = [];
75
+ for (const line of lines.slice(start + 1)) {
76
+ if (line.trim().startsWith("## ")) {
77
+ break;
78
+ }
79
+ if (line.trim().startsWith("- ")) {
80
+ section.push(line.trim().slice(2).trim());
81
+ }
82
+ }
83
+ return section;
84
+ };
85
+ const programArtifactsJsonSchema = {
86
+ type: "object",
87
+ properties: {
88
+ spec: { type: "string" },
89
+ feature: { type: "string" },
90
+ traceability: { type: "string" },
91
+ mapping: { type: "string" }
92
+ },
93
+ required: ["spec", "feature", "traceability", "mapping"]
94
+ };
95
+ const programAsk = (pack, relativePath) => [
96
+ `Extract the behavioural spec for ONE source unit of this repository: ${relativePath}`,
97
+ "",
98
+ "Read the source file and anything it references (copybooks, includes, called programs)",
99
+ `for context, but spec ONLY ${relativePath} and do not modify legacy sources.`,
100
+ "",
101
+ 'Respond only with JSON: {"spec":"…","feature":"…","traceability":"…","mapping":"…"} where:',
102
+ "",
103
+ `- "spec" — the behavioural spec for ${relativePath}, as Markdown.`,
104
+ pack.prompt("spec") ?? "",
105
+ "",
106
+ `- "feature" — BDD scenarios encoding that spec, as a well-formed Gherkin .feature file`,
107
+ " (Feature: header, Scenario: blocks, Given/When/Then steps).",
108
+ pack.prompt("bdd") ?? "",
109
+ "",
110
+ `- "traceability" — EVERY source unit of ${relativePath} (each COBOL paragraph, each JCL`,
111
+ " step) on its own line, mapped to the spec rules/scenarios that cover it:",
112
+ " `<UNIT-NAME> — <refs>`. Unit names verbatim as they appear in the source.",
113
+ "",
114
+ `- "mapping" — data & interface mapping for ${relativePath}: tables/record layouts → target`,
115
+ " entities; files/screens/queues → target service contracts."
116
+ ].join("\n");
117
+ /** Sub-bar judge dimensions as Critical review issues, titled with their program. */
118
+ const judgeIssues = (pack, scored, program) => {
119
+ const issues = scored.scores.flatMap((score) => {
120
+ const maxScore = pack.judgeDimensions.find((d) => d.name === score.name)?.maxScore ?? 2;
121
+ return score.score < maxScore
122
+ ? [
123
+ ReviewIssue.make({
124
+ severity: "Critical",
125
+ title: `judge[${program}]: ${score.name} scored ${score.score}`,
126
+ description: score.reasoning
127
+ })
128
+ ]
129
+ : [];
130
+ });
131
+ return ReviewResult.make({ issues, summary: `judge:${program}` });
132
+ };
133
+ const judgeIssueProgram = (issue) => /^judge\[([^\]]+)\]: /.exec(issue.title)?.[1];
134
+ const issueLines = (issues) => issues.map((issue) => `- [${issue.severity}] ${issue.title}: ${issue.description}`).join("\n");
135
+ const programFixAsk = (name, relativePath, issues) => [
136
+ `The spec pack for ONE program did not clear its quality gate: ${name} (source: ${relativePath}).`,
137
+ `Fix these findings by editing ONLY this program's files — ${ModDir}/specs/${name}.md,`,
138
+ `${ModDir}/features/${name.toLowerCase()}.feature, ${ModDir}/traceability/${name}.md,`,
139
+ `${ModDir}/mapping/${name}.md — against the source at ${relativePath}. Then stop:`,
140
+ issueLines(issues)
141
+ ].join("\n");
142
+ const globalFixAsk = (issues) => [
143
+ "The spec pack did not clear its estate-wide quality gate. Fix these findings by editing the",
144
+ `per-program files under ${ModDir}/ (specs/, features/, traceability/<PROGRAM>.md,`,
145
+ `mapping/<PROGRAM>.md). ${ModDir}/traceability.md and ${ModDir}/mapping.md are REGENERATED`,
146
+ "from the fragments — do not edit them directly. Fix the findings in place, then stop:",
147
+ issueLines(issues)
148
+ ].join("\n");
149
+ const readmeFor = (pack, verdict) => [
150
+ `# Modernization spec pack — ${pack.name}`,
151
+ "",
152
+ `Extracted by the modernize-extract flow. Gate verdict: ${verdict}.`,
153
+ "",
154
+ "- specs/ — behavioural specs, one per program",
155
+ "- features/ — BDD acceptance scenarios",
156
+ "- traceability.md — source-unit → spec coverage matrix (generated from traceability/)",
157
+ "- mapping.md — data & interface mapping (generated from mapping/)",
158
+ "- rules.txt — every coverage unit, one per line (the rule universe verification reports against)",
159
+ "- plan.md — proposed implementation tasks",
160
+ "",
161
+ "Review everything, then flip the marker below and run the seed phase."
162
+ ].join("\n");
163
+ const program = Effect.gen(function* () {
164
+ const input = yield* resolveFlowInput("Extract the complete behavioural spec pack for this legacy estate");
165
+ const coder = coderFromEnv(process.env);
166
+ const files = nodePlainFileStore;
167
+ const modDirAbs = join(input.workDir, ModDir);
168
+ yield* runNode({
169
+ workDir: input.workDir,
170
+ workspace: input.workspace,
171
+ userPrompt: input.prompt,
172
+ coder,
173
+ reasoning: asReadOnly(coder),
174
+ environment: process.env
175
+ }, (context) => Effect.gen(function* () {
176
+ const repo = yield* makeNodeWorkspace(input.workDir);
177
+ const opened = yield* stage(context.events, "pack", openPack({
178
+ environment: process.env,
179
+ launchDir: input.workspace,
180
+ flowDir: import.meta.dirname
181
+ }));
182
+ const pack = opened.pack;
183
+ yield* stage(context.events, "branch", context.git.checkoutOrCreate("modernize/spec-pack").pipe(Effect.asVoid));
184
+ const system = [
185
+ pack.prompt("analysis"),
186
+ pack.lessons === undefined
187
+ ? undefined
188
+ : `Lessons from previous modernization runs — apply them:\n${pack.lessons}`
189
+ ]
190
+ .filter((part) => part !== undefined)
191
+ .join("\n\n");
192
+ const all = yield* stage(context.events, "inventory", matchingFiles(repo, pack.programs ?? pack.sources ?? ".*"));
193
+ const wave = process.env.LLM4TS_WAVE?.trim();
194
+ const programs = wave === undefined || wave.length === 0
195
+ ? all
196
+ : yield* Effect.gen(function* () {
197
+ yield* stage(context.events, "wave approval", requireApproval(files, join(modDirAbs, "wave-plan.md")));
198
+ const planText = (yield* files.read(join(modDirAbs, "wave-plan.md"))) ?? "";
199
+ const names = new Set(wavePrograms(planText, wave));
200
+ const scoped = all.filter((rel) => names.has(programName(rel)));
201
+ if (scoped.length === 0) {
202
+ return yield* FlowAborted.make({
203
+ message: `wave '${wave}' matches no programs in ${ModDir}/wave-plan.md`
204
+ });
205
+ }
206
+ return scoped;
207
+ });
208
+ if (programs.length === 0) {
209
+ return yield* FlowAborted.make({
210
+ message: `no source units matched the pack's programs/sources regex under ${input.workDir}`
211
+ });
212
+ }
213
+ const units = programs.map((rel) => ProgramUnit.make({ name: programName(rel), sourcePath: rel }));
214
+ // Pattern cards are selected deterministically from the SOURCE, never by
215
+ // the model: implementation later injects exactly the cards cited here.
216
+ const cards = [
217
+ ...(yield* loadPatternCards(opened.workspace, `${opened.dir}/patterns`)),
218
+ ...(yield* loadUniversalPatternCards([input.workspace, import.meta.dirname]))
219
+ ];
220
+ // One structured analyst call per program, resumable per program: a rerun
221
+ // skips every program whose spec exists, and each program gets its own commit.
222
+ yield* stage(context.events, "extract", Effect.gen(function* () {
223
+ for (const [index, unit] of units.entries()) {
224
+ const summary = yield* extractProgramsResumably(files, [unit], (target) => Effect.gen(function* () {
225
+ yield* context.events.publish(Info.make({
226
+ message: `extracting ${target.sourcePath} (${index + 1}/${units.length})`
227
+ }));
228
+ return yield* context.coder
229
+ .executeStructured(`${system}\n\n${programAsk(pack, target.sourcePath)}`, ProgramArtifacts, programArtifactsJsonSchema)
230
+ .pipe(Effect.mapError(FlowLlmError.from),
231
+ // A turn-limit trip is the wedged-agent tail, not a
232
+ // failure: keep whatever the analyst already produced.
233
+ Effect.catchIf((error) => error.cause?._tag === "TurnLimitError", (error) => Effect.gen(function* () {
234
+ const existing = yield* files.read(join(modDirAbs, "specs", `${target.name}.md`));
235
+ if (existing === undefined) {
236
+ return yield* Effect.fail(error);
237
+ }
238
+ yield* context.events.publish(Info.make({
239
+ message: `turn limit hit on ${target.sourcePath} after its spec was written — keeping the work`
240
+ }));
241
+ return ProgramArtifacts.make({
242
+ spec: existing,
243
+ feature: "",
244
+ traceability: "",
245
+ mapping: ""
246
+ });
247
+ })));
248
+ }), modDirAbs);
249
+ if (summary.created.length > 0) {
250
+ // Tag the traceability fragment with the cards this program's
251
+ // source matches — regex-decided, so implementation's playbook
252
+ // is reproducible.
253
+ const source = yield* files.read(join(input.workDir, unit.sourcePath));
254
+ const matched = source === undefined ? [] : matchingPatternCards(source, cards);
255
+ if (matched.length > 0) {
256
+ const fragmentPath = join(modDirAbs, "traceability", `${unit.name}.md`);
257
+ const fragment = (yield* files.read(fragmentPath)) ?? "";
258
+ yield* files.writeAtomic(fragmentPath, `${fragment.trimEnd()}\n\nPatterns: ${matched.map((card) => card.id).join(", ")}\n`);
259
+ }
260
+ yield* context.git
261
+ .commitAll(`modernize(${pack.name}): spec ${unit.name}`)
262
+ .pipe(Effect.asVoid);
263
+ }
264
+ else {
265
+ yield* context.events.publish(Info.make({
266
+ message: `resume: specs/${unit.name}.md exists — skipping ${unit.sourcePath}`
267
+ }));
268
+ }
269
+ }
270
+ }));
271
+ // traceability.md / mapping.md are regenerated from the fragments — fixes
272
+ // belong in the fragments, never the indexes.
273
+ const rebuildIndexes = Effect.gen(function* () {
274
+ for (const [fragmentDir, index] of [
275
+ ["traceability", "traceability.md"],
276
+ ["mapping", "mapping.md"]
277
+ ]) {
278
+ const parts = [];
279
+ for (const unit of units) {
280
+ const text = yield* files.read(join(modDirAbs, fragmentDir, `${unit.name}.md`));
281
+ if (text !== undefined && text.trim().length > 0) {
282
+ parts.push(`===== ${unit.name} =====\n${text.trimEnd()}`);
283
+ }
284
+ }
285
+ if (parts.length > 0) {
286
+ yield* files.writeAtomic(join(modDirAbs, index), parts.join("\n\n") + "\n");
287
+ }
288
+ }
289
+ });
290
+ const writeRules = Effect.gen(function* () {
291
+ const unitsByRule = yield* coverageUnits(repo, pack.coverage);
292
+ const allUnits = [...new Set(Object.values(unitsByRule).flat())].sort();
293
+ if (allUnits.length > 0) {
294
+ yield* files.writeAtomic(join(modDirAbs, "rules.txt"), allUnits.join("\n") + "\n");
295
+ }
296
+ });
297
+ // Commit the (ungated) draft: extraction is the expensive step, and a gate
298
+ // failure or crash must not cost it.
299
+ yield* stage(context.events, "draft", rebuildIndexes.pipe(Effect.andThen(writeRules), Effect.andThen(context.git
300
+ .commitAll(`modernize(${pack.name}): spec pack draft (ungated)`)
301
+ .pipe(Effect.asVoid))));
302
+ const packJudge = judge(context.reasoning, pack.judgeDimensions);
303
+ const limit = judgeSourcesLimit();
304
+ /**
305
+ * An empty structured response that survives the in-run retries is
306
+ * usually DETERMINISTIC (context overflow), so repeating the same
307
+ * prompt cannot succeed — retry at half, then quarter context instead.
308
+ */
309
+ const judgeWithShrink = (spec, feature, source) => {
310
+ const attempt = (cap, rest) => packJudge
311
+ .evaluate(Sample.make({
312
+ response: capText(`${spec}\n\n${feature}`, cap),
313
+ context: capText(source, cap),
314
+ query: input.prompt
315
+ }))
316
+ .pipe(Effect.mapError(FlowLlmError.from), Effect.catchIf((error) => rest.length > 0 && error.message.includes("empty response"), (error) => {
317
+ const [next, ...remaining] = rest;
318
+ return context.events
319
+ .publish(Info.make({
320
+ message: `judge returned empty at cap ${cap} chars — shrinking to ${next ?? cap}: ${error.message}`
321
+ }))
322
+ .pipe(Effect.andThen(attempt(next ?? cap, remaining)));
323
+ }));
324
+ return attempt(limit, [Math.floor(limit / 2), Math.floor(limit / 4)]);
325
+ };
326
+ /**
327
+ * Judging is resumable per program: the verdict persists under
328
+ * `gate/<NAME>.json`, fingerprinted over the source, spec, feature, and
329
+ * rubric it judged. Unchanged content reuses the stored verdict with NO
330
+ * model call, so a crash or quota death re-judges only what changed.
331
+ * Delete `gate/` to force a full re-judge.
332
+ */
333
+ const judgeProgram = (unit) => Effect.gen(function* () {
334
+ const spec = (yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))) ?? "";
335
+ const feature = (yield* files.read(join(modDirAbs, "features", `${unit.name.toLowerCase()}.feature`))) ?? "";
336
+ const source = (yield* files.read(join(input.workDir, unit.sourcePath))) ?? "";
337
+ const rubric = pack.judgeDimensions
338
+ .map((dimension) => `${dimension.name} (0..${dimension.maxScore}): ${dimension.rubric}`)
339
+ .join("\n");
340
+ return yield* cachedReview(files, join(modDirAbs, "gate", `${unit.name}.json`), reviewFingerprint(source, spec, feature, rubric), context.events.publish(Info.make({ message: `judging ${unit.name}` })).pipe(Effect.andThen(judgeWithShrink(spec, feature, source)), Effect.map((scored) => judgeIssues(pack, scored, unit.name))));
341
+ });
342
+ const gateEvaluate = Effect.gen(function* () {
343
+ yield* rebuildIndexes;
344
+ const trace = (yield* files.read(join(modDirAbs, "traceability.md"))) ?? "";
345
+ const mapping = (yield* files.read(join(modDirAbs, "mapping.md"))) ?? "";
346
+ const docs = ReviewResult.make({
347
+ issues: [
348
+ ...(trace.trim().length === 0
349
+ ? [
350
+ ReviewIssue.make({
351
+ severity: "Critical",
352
+ title: "missing traceability",
353
+ description: `no ${ModDir}/traceability/ fragments were written`
354
+ })
355
+ ]
356
+ : []),
357
+ ...(mapping.trim().length === 0
358
+ ? [
359
+ ReviewIssue.make({
360
+ severity: "Critical",
361
+ title: "missing mapping",
362
+ description: `no ${ModDir}/mapping/ fragments were written`
363
+ })
364
+ ]
365
+ : [])
366
+ ],
367
+ summary: "docs"
368
+ });
369
+ const covered = yield* coverage(repo, pack.coverage, trace);
370
+ const wellFormed = yield* features(repo, join(ModDir, "features"));
371
+ const judged = [];
372
+ for (const unit of units) {
373
+ judged.push(yield* judgeProgram(unit));
374
+ }
375
+ return mergeReviewResults([covered, wellFormed, docs, ...judged]);
376
+ });
377
+ // One bounded fix turn per sub-bar program (own commit) plus one residual
378
+ // estate-wide turn, then re-evaluate — up to MaxRounds evaluations.
379
+ const fixOnce = (result) => Effect.gen(function* () {
380
+ const relOf = new Map(units.map((unit) => [unit.name, unit.sourcePath]));
381
+ const scoped = new Map();
382
+ const global = [];
383
+ for (const issue of result.issues) {
384
+ const name = judgeIssueProgram(issue);
385
+ if (name !== undefined && relOf.has(name)) {
386
+ const bucket = scoped.get(name) ?? [];
387
+ bucket.push(issue);
388
+ scoped.set(name, bucket);
389
+ }
390
+ else {
391
+ global.push(issue);
392
+ }
393
+ }
394
+ const turn = (ask, commitMessage) => Effect.gen(function* () {
395
+ const chat = yield* makeChat(context.coder, { system });
396
+ yield* chat.ask(ask).pipe(Effect.asVoid,
397
+ // A wedged agent that trips its turn limit mid-fix still wrote
398
+ // something; re-evaluate what landed instead of failing.
399
+ Effect.catchIf((error) => error._tag === "Llm" && error.cause?._tag === "TurnLimitError", () => context.events.publish(Info.make({
400
+ message: "turn limit hit during a fix turn — re-evaluating what was written"
401
+ }))));
402
+ yield* context.git.commitAll(commitMessage).pipe(Effect.asVoid);
403
+ });
404
+ for (const [name, issues] of [...scoped.entries()].sort()) {
405
+ yield* context.events.publish(Info.make({ message: `fixing ${name} — ${issues.length} finding(s)` }));
406
+ const relativePath = relOf.get(name);
407
+ if (relativePath !== undefined) {
408
+ yield* turn(programFixAsk(name, relativePath, issues), `modernize(${pack.name}): gate fixes ${name}`);
409
+ }
410
+ }
411
+ if (global.length > 0) {
412
+ yield* context.events.publish(Info.make({ message: `fixing estate-wide findings — ${global.length}` }));
413
+ yield* turn(globalFixAsk(global), `modernize(${pack.name}): gate fixes (estate-wide)`);
414
+ }
415
+ });
416
+ let result = yield* stage(context.events, "gate", gateEvaluate);
417
+ for (let round = 2; round <= MaxRounds && result.issues.length > 0; round += 1) {
418
+ yield* stage(context.events, `gate fixes (round ${round})`, fixOnce(result));
419
+ result = yield* stage(context.events, `gate (round ${round})`, gateEvaluate);
420
+ }
421
+ if (result.issues.length === 0) {
422
+ yield* stage(context.events, "plan", Effect.gen(function* () {
423
+ const specTexts = [];
424
+ for (const unit of units) {
425
+ const spec = yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`));
426
+ if (spec !== undefined) {
427
+ specTexts.push(spec);
428
+ }
429
+ }
430
+ const plan = yield* planFrom(context.reasoning, capText(specTexts.join("\n\n"), limit), `${defaultPlanInstructions}\n\n${pack.prompt("plan") ?? ""}`);
431
+ yield* files.writeAtomic(join(modDirAbs, "plan.md"), plan.render);
432
+ }));
433
+ }
434
+ const verdict = result.issues.length === 0
435
+ ? "PASSED — pending human approval"
436
+ : `DRAFT — ${result.issues.length} open issue(s)`;
437
+ yield* stage(context.events, "commit", files
438
+ .writeAtomic(join(modDirAbs, "README.md"), withDraftApproval(readmeFor(pack, verdict)))
439
+ .pipe(Effect.andThen(context.git
440
+ .commitAll(`modernize(${pack.name}): spec pack (${verdict})`)
441
+ .pipe(Effect.asVoid))));
442
+ if (result.issues.length > 0) {
443
+ return yield* FlowAborted.make({
444
+ message: `extraction gate not cleared after ${MaxRounds} round(s) — spec pack committed as draft:\n` +
445
+ result.issues.map((issue) => `- ${issue.title}`).join("\n")
446
+ });
447
+ }
448
+ yield* context.events.publish(Info.make({
449
+ message: `spec pack ready — review ${ModDir}/README.md, set '- [x] Approved', ` +
450
+ "then run the seed phase"
451
+ }));
452
+ }));
453
+ });
454
+ runFlowMain(program);