@llm4ts/shell 0.2.0 → 0.2.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Riccardo Merolla
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/Cli.d.ts CHANGED
@@ -27,7 +27,7 @@ export declare const renderFlowList: (flows: ReadonlyArray<DiscoveredFlow>, opti
27
27
  * else is looked up by name in the discovery listing.
28
28
  */
29
29
  export declare const resolveFlow: (reference: string, tiers: FlowTierPaths) => Effect.Effect<string, ShellUsageError, FileSystem>;
30
- export declare const shellCommand: Command.Command<"llm4ts", {} | {}, {}, ShellUsageError | ShellActionError | import("./FlowLaunch.ts").FlowLaunchError, import("effect/unstable/cli/Prompt").Environment>;
30
+ export declare const shellCommand: Command.Command<"llm4ts", {} | {}, {}, import("./FlowLaunch.ts").FlowLaunchError | ShellUsageError | ShellActionError, import("effect/unstable/cli/Prompt").Environment>;
31
31
  export declare const runShellCommand: (argv: ReadonlyArray<string>) => Effect.Effect<void, unknown, Command.Environment>;
32
32
  export {};
33
33
  //# sourceMappingURL=Cli.d.ts.map
package/dist/cli-main.js CHANGED
File without changes
@@ -0,0 +1,522 @@
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). LLM4TS_WAVE=<name> scopes the run to one wave of the approved
23
+ // plan. Judge context is bounded by LLM4TS_JUDGE_SOURCES_LIMIT (chars).
24
+ import { join } from "node:path"
25
+ import * as Effect from "effect/Effect"
26
+ import { Sample } from "@llm4ts/core/eval/Eval"
27
+ import { judge } from "@llm4ts/core/eval/Judge"
28
+ import type { JsonSchema } from "@llm4ts/core/Models"
29
+ import { FlowAborted, FlowLlmError } from "@llm4ts/flow/FlowError"
30
+ import { Info } from "@llm4ts/flow/FlowEvents"
31
+ import { makeChat } from "@llm4ts/flow/Chat"
32
+ import { loadPack, type Pack } from "@llm4ts/flow/Pack"
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 { coverage, coverageUnits, features, matchingFiles } from "@llm4ts/flow/SpecChecks"
37
+ import { withDraftApproval, requireApproval } from "@llm4ts/modernize/Approval"
38
+ import {
39
+ ProgramArtifacts,
40
+ ProgramUnit,
41
+ extractProgramsResumably
42
+ } from "@llm4ts/modernize/Artifacts"
43
+ import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors"
44
+ import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
45
+ import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
46
+ import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
47
+ import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
48
+
49
+ const ModDir = "docs/modernization"
50
+ const MaxRounds = 3
51
+
52
+ const judgeSourcesLimit = (): number => {
53
+ const raw = Number.parseInt(process.env.LLM4TS_JUDGE_SOURCES_LIMIT ?? "", 10)
54
+ return Number.isFinite(raw) && raw > 0 ? raw : 400_000
55
+ }
56
+
57
+ /** Past the limit keep head + tail so entry points and trailing rules stay visible. */
58
+ const capText = (text: string, limit: number): string => {
59
+ if (text.length <= limit) {
60
+ return text
61
+ }
62
+ const head = Math.floor((limit * 3) / 4)
63
+ return `${text.slice(0, head)}\n\n… [truncated] …\n\n${text.slice(text.length - (limit - head))}`
64
+ }
65
+
66
+ /** `cobol/ACCTXFR.cbl` → `ACCTXFR`: the program name keying every per-program artifact. */
67
+ const programName = (relativePath: string): string => {
68
+ const base = relativePath.slice(relativePath.lastIndexOf("/") + 1)
69
+ const dot = base.lastIndexOf(".")
70
+ return dot > 0 ? base.slice(0, dot) : base
71
+ }
72
+
73
+ /** The `- PROG` entries of `## Wave: <name>` in the survey's wave plan. */
74
+ const wavePrograms = (planText: string, wave: string): ReadonlyArray<string> => {
75
+ const lines = planText.split(/\r?\n/)
76
+ const start = lines.findIndex((line) => line.trim() === `## Wave: ${wave}`)
77
+ if (start < 0) {
78
+ return []
79
+ }
80
+ const section: Array<string> = []
81
+ for (const line of lines.slice(start + 1)) {
82
+ if (line.trim().startsWith("## ")) {
83
+ break
84
+ }
85
+ if (line.trim().startsWith("- ")) {
86
+ section.push(line.trim().slice(2).trim())
87
+ }
88
+ }
89
+ return section
90
+ }
91
+
92
+ const programArtifactsJsonSchema: JsonSchema = {
93
+ type: "object",
94
+ properties: {
95
+ spec: { type: "string" },
96
+ feature: { type: "string" },
97
+ traceability: { type: "string" },
98
+ mapping: { type: "string" }
99
+ },
100
+ required: ["spec", "feature", "traceability", "mapping"]
101
+ }
102
+
103
+ const programAsk = (pack: Pack, relativePath: string): string =>
104
+ [
105
+ `Extract the behavioural spec for ONE source unit of this repository: ${relativePath}`,
106
+ "",
107
+ "Read the source file and anything it references (copybooks, includes, called programs)",
108
+ `for context, but spec ONLY ${relativePath} and do not modify legacy sources.`,
109
+ "",
110
+ 'Respond only with JSON: {"spec":"…","feature":"…","traceability":"…","mapping":"…"} where:',
111
+ "",
112
+ `- "spec" — the behavioural spec for ${relativePath}, as Markdown.`,
113
+ pack.prompt("spec") ?? "",
114
+ "",
115
+ `- "feature" — BDD scenarios encoding that spec, as a well-formed Gherkin .feature file`,
116
+ " (Feature: header, Scenario: blocks, Given/When/Then steps).",
117
+ pack.prompt("bdd") ?? "",
118
+ "",
119
+ `- "traceability" — EVERY source unit of ${relativePath} (each COBOL paragraph, each JCL`,
120
+ " step) on its own line, mapped to the spec rules/scenarios that cover it:",
121
+ " `<UNIT-NAME> — <refs>`. Unit names verbatim as they appear in the source.",
122
+ "",
123
+ `- "mapping" — data & interface mapping for ${relativePath}: tables/record layouts → target`,
124
+ " entities; files/screens/queues → target service contracts."
125
+ ].join("\n")
126
+
127
+ /** Sub-bar judge dimensions as Critical review issues, titled with their program. */
128
+ const judgeIssues = (
129
+ pack: Pack,
130
+ scored: {
131
+ readonly scores: ReadonlyArray<{
132
+ readonly name: string
133
+ readonly score: number
134
+ readonly reasoning: string
135
+ }>
136
+ },
137
+ program: string
138
+ ): ReviewResult => {
139
+ const issues = scored.scores.flatMap((score) => {
140
+ const maxScore = pack.judgeDimensions.find((d) => d.name === score.name)?.maxScore ?? 2
141
+ return score.score < maxScore
142
+ ? [
143
+ ReviewIssue.make({
144
+ severity: "Critical",
145
+ title: `judge[${program}]: ${score.name} scored ${score.score}`,
146
+ description: score.reasoning
147
+ })
148
+ ]
149
+ : []
150
+ })
151
+ return ReviewResult.make({ issues, summary: `judge:${program}` })
152
+ }
153
+
154
+ const judgeIssueProgram = (issue: ReviewIssue): string | undefined =>
155
+ /^judge\[([^\]]+)\]: /.exec(issue.title)?.[1]
156
+
157
+ const issueLines = (issues: ReadonlyArray<ReviewIssue>): string =>
158
+ issues.map((issue) => `- [${issue.severity}] ${issue.title}: ${issue.description}`).join("\n")
159
+
160
+ const programFixAsk = (
161
+ name: string,
162
+ relativePath: string,
163
+ issues: ReadonlyArray<ReviewIssue>
164
+ ): string =>
165
+ [
166
+ `The spec pack for ONE program did not clear its quality gate: ${name} (source: ${relativePath}).`,
167
+ `Fix these findings by editing ONLY this program's files — ${ModDir}/specs/${name}.md,`,
168
+ `${ModDir}/features/${name.toLowerCase()}.feature, ${ModDir}/traceability/${name}.md,`,
169
+ `${ModDir}/mapping/${name}.md — against the source at ${relativePath}. Then stop:`,
170
+ issueLines(issues)
171
+ ].join("\n")
172
+
173
+ const globalFixAsk = (issues: ReadonlyArray<ReviewIssue>): string =>
174
+ [
175
+ "The spec pack did not clear its estate-wide quality gate. Fix these findings by editing the",
176
+ `per-program files under ${ModDir}/ (specs/, features/, traceability/<PROGRAM>.md,`,
177
+ `mapping/<PROGRAM>.md). ${ModDir}/traceability.md and ${ModDir}/mapping.md are REGENERATED`,
178
+ "from the fragments — do not edit them directly. Fix the findings in place, then stop:",
179
+ issueLines(issues)
180
+ ].join("\n")
181
+
182
+ const readmeFor = (pack: Pack, verdict: string): string =>
183
+ [
184
+ `# Modernization spec pack — ${pack.name}`,
185
+ "",
186
+ `Extracted by the modernize-extract flow. Gate verdict: ${verdict}.`,
187
+ "",
188
+ "- specs/ — behavioural specs, one per program",
189
+ "- features/ — BDD acceptance scenarios",
190
+ "- traceability.md — source-unit → spec coverage matrix (generated from traceability/)",
191
+ "- mapping.md — data & interface mapping (generated from mapping/)",
192
+ "- rules.txt — every coverage unit, one per line (the rule universe verification reports against)",
193
+ "- plan.md — proposed implementation tasks",
194
+ "",
195
+ "Review everything, then flip the marker below and run the seed phase."
196
+ ].join("\n")
197
+
198
+ const program = Effect.gen(function* () {
199
+ const input = yield* resolveFlowInput(
200
+ "Extract the complete behavioural spec pack for this legacy estate"
201
+ )
202
+ const packDir = process.env.LLM4TS_PACK ?? "packs/cobol-springboot"
203
+ const coder = coderFromEnv(process.env)
204
+ const files = nodePlainFileStore
205
+ const modDirAbs = join(input.workDir, ModDir)
206
+
207
+ yield* runNode(
208
+ {
209
+ workDir: input.workDir,
210
+ workspace: input.workspace,
211
+ userPrompt: input.prompt,
212
+ coder,
213
+ reasoning: asReadOnly(coder),
214
+ environment: process.env
215
+ },
216
+ (context) =>
217
+ Effect.gen(function* () {
218
+ const launchWorkspace = yield* makeNodeWorkspace(input.workspace)
219
+ const repo = yield* makeNodeWorkspace(input.workDir)
220
+ const pack = yield* stage(context.events, "pack", loadPack(launchWorkspace, packDir))
221
+ yield* stage(
222
+ context.events,
223
+ "branch",
224
+ context.git.checkoutOrCreate("modernize/spec-pack").pipe(Effect.asVoid)
225
+ )
226
+ const system = [
227
+ pack.prompt("analysis"),
228
+ pack.lessons === undefined
229
+ ? undefined
230
+ : `Lessons from previous modernization runs — apply them:\n${pack.lessons}`
231
+ ]
232
+ .filter((part) => part !== undefined)
233
+ .join("\n\n")
234
+
235
+ const all = yield* stage(
236
+ context.events,
237
+ "inventory",
238
+ matchingFiles(repo, pack.programs ?? pack.sources ?? ".*")
239
+ )
240
+ const wave = process.env.LLM4TS_WAVE?.trim()
241
+ const programs =
242
+ wave === undefined || wave.length === 0
243
+ ? all
244
+ : yield* Effect.gen(function* () {
245
+ yield* stage(
246
+ context.events,
247
+ "wave approval",
248
+ requireApproval(files, join(modDirAbs, "wave-plan.md"))
249
+ )
250
+ const planText = (yield* files.read(join(modDirAbs, "wave-plan.md"))) ?? ""
251
+ const names = new Set(wavePrograms(planText, wave))
252
+ const scoped = all.filter((rel) => names.has(programName(rel)))
253
+ if (scoped.length === 0) {
254
+ return yield* FlowAborted.make({
255
+ message: `wave '${wave}' matches no programs in ${ModDir}/wave-plan.md`
256
+ })
257
+ }
258
+ return scoped
259
+ })
260
+ if (programs.length === 0) {
261
+ return yield* FlowAborted.make({
262
+ message: `no source units matched the pack's programs/sources regex under ${input.workDir}`
263
+ })
264
+ }
265
+
266
+ const units = programs.map((rel) =>
267
+ ProgramUnit.make({ name: programName(rel), sourcePath: rel })
268
+ )
269
+
270
+ // One structured analyst call per program, resumable per program: a rerun
271
+ // skips every program whose spec exists, and each program gets its own commit.
272
+ yield* stage(
273
+ context.events,
274
+ "extract",
275
+ Effect.gen(function* () {
276
+ for (const [index, unit] of units.entries()) {
277
+ const summary = yield* extractProgramsResumably(
278
+ files,
279
+ [unit],
280
+ (target) =>
281
+ Effect.gen(function* () {
282
+ yield* context.events.publish(
283
+ Info.make({
284
+ message: `extracting ${target.sourcePath} (${index + 1}/${units.length})`
285
+ })
286
+ )
287
+ return yield* context.coder
288
+ .executeStructured(
289
+ `${system}\n\n${programAsk(pack, target.sourcePath)}`,
290
+ ProgramArtifacts,
291
+ programArtifactsJsonSchema
292
+ )
293
+ .pipe(Effect.mapError(FlowLlmError.from))
294
+ }),
295
+ modDirAbs
296
+ )
297
+ if (summary.created.length > 0) {
298
+ yield* context.git
299
+ .commitAll(`modernize(${pack.name}): spec ${unit.name}`)
300
+ .pipe(Effect.asVoid)
301
+ } else {
302
+ yield* context.events.publish(
303
+ Info.make({
304
+ message: `resume: specs/${unit.name}.md exists — skipping ${unit.sourcePath}`
305
+ })
306
+ )
307
+ }
308
+ }
309
+ })
310
+ )
311
+
312
+ // traceability.md / mapping.md are regenerated from the fragments — fixes
313
+ // belong in the fragments, never the indexes.
314
+ const rebuildIndexes = Effect.gen(function* () {
315
+ for (const [fragmentDir, index] of [
316
+ ["traceability", "traceability.md"],
317
+ ["mapping", "mapping.md"]
318
+ ] as const) {
319
+ const parts: Array<string> = []
320
+ for (const unit of units) {
321
+ const text = yield* files.read(join(modDirAbs, fragmentDir, `${unit.name}.md`))
322
+ if (text !== undefined && text.trim().length > 0) {
323
+ parts.push(`===== ${unit.name} =====\n${text.trimEnd()}`)
324
+ }
325
+ }
326
+ if (parts.length > 0) {
327
+ yield* files.writeAtomic(join(modDirAbs, index), parts.join("\n\n") + "\n")
328
+ }
329
+ }
330
+ })
331
+
332
+ const writeRules = Effect.gen(function* () {
333
+ const unitsByRule = yield* coverageUnits(repo, pack.coverage)
334
+ const allUnits = [...new Set(Object.values(unitsByRule).flat())].sort()
335
+ if (allUnits.length > 0) {
336
+ yield* files.writeAtomic(join(modDirAbs, "rules.txt"), allUnits.join("\n") + "\n")
337
+ }
338
+ })
339
+
340
+ // Commit the (ungated) draft: extraction is the expensive step, and a gate
341
+ // failure or crash must not cost it.
342
+ yield* stage(
343
+ context.events,
344
+ "draft",
345
+ rebuildIndexes.pipe(
346
+ Effect.andThen(writeRules),
347
+ Effect.andThen(
348
+ context.git
349
+ .commitAll(`modernize(${pack.name}): spec pack draft (ungated)`)
350
+ .pipe(Effect.asVoid)
351
+ )
352
+ )
353
+ )
354
+
355
+ const packJudge = judge(context.reasoning, pack.judgeDimensions)
356
+ const limit = judgeSourcesLimit()
357
+
358
+ const judgeProgram = (unit: ProgramUnit) =>
359
+ Effect.gen(function* () {
360
+ const spec = (yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))) ?? ""
361
+ const feature =
362
+ (yield* files.read(
363
+ join(modDirAbs, "features", `${unit.name.toLowerCase()}.feature`)
364
+ )) ?? ""
365
+ const source = (yield* files.read(join(input.workDir, unit.sourcePath))) ?? ""
366
+ yield* context.events.publish(Info.make({ message: `judging ${unit.name}` }))
367
+ const scored = yield* packJudge
368
+ .evaluate(
369
+ Sample.make({
370
+ response: capText(`${spec}\n\n${feature}`, limit),
371
+ context: capText(source, limit),
372
+ query: input.prompt
373
+ })
374
+ )
375
+ .pipe(Effect.mapError(FlowLlmError.from))
376
+ return judgeIssues(pack, scored, unit.name)
377
+ })
378
+
379
+ const gateEvaluate = Effect.gen(function* () {
380
+ yield* rebuildIndexes
381
+ const trace = (yield* files.read(join(modDirAbs, "traceability.md"))) ?? ""
382
+ const mapping = (yield* files.read(join(modDirAbs, "mapping.md"))) ?? ""
383
+ const docs = ReviewResult.make({
384
+ issues: [
385
+ ...(trace.trim().length === 0
386
+ ? [
387
+ ReviewIssue.make({
388
+ severity: "Critical",
389
+ title: "missing traceability",
390
+ description: `no ${ModDir}/traceability/ fragments were written`
391
+ })
392
+ ]
393
+ : []),
394
+ ...(mapping.trim().length === 0
395
+ ? [
396
+ ReviewIssue.make({
397
+ severity: "Critical",
398
+ title: "missing mapping",
399
+ description: `no ${ModDir}/mapping/ fragments were written`
400
+ })
401
+ ]
402
+ : [])
403
+ ],
404
+ summary: "docs"
405
+ })
406
+ const covered = yield* coverage(repo, pack.coverage, trace)
407
+ const wellFormed = yield* features(repo, join(ModDir, "features"))
408
+ const judged: Array<ReviewResult> = []
409
+ for (const unit of units) {
410
+ judged.push(yield* judgeProgram(unit))
411
+ }
412
+ return mergeReviewResults([covered, wellFormed, docs, ...judged])
413
+ })
414
+
415
+ // One bounded fix turn per sub-bar program (own commit) plus one residual
416
+ // estate-wide turn, then re-evaluate — up to MaxRounds evaluations.
417
+ const fixOnce = (result: ReviewResult) =>
418
+ Effect.gen(function* () {
419
+ const relOf = new Map(units.map((unit) => [unit.name, unit.sourcePath]))
420
+ const scoped = new Map<string, Array<ReviewIssue>>()
421
+ const global: Array<ReviewIssue> = []
422
+ for (const issue of result.issues) {
423
+ const name = judgeIssueProgram(issue)
424
+ if (name !== undefined && relOf.has(name)) {
425
+ const bucket = scoped.get(name) ?? []
426
+ bucket.push(issue)
427
+ scoped.set(name, bucket)
428
+ } else {
429
+ global.push(issue)
430
+ }
431
+ }
432
+ const turn = (ask: string, commitMessage: string) =>
433
+ Effect.gen(function* () {
434
+ const chat = yield* makeChat(context.coder, { system })
435
+ yield* chat.ask(ask)
436
+ yield* context.git.commitAll(commitMessage).pipe(Effect.asVoid)
437
+ })
438
+ for (const [name, issues] of [...scoped.entries()].sort()) {
439
+ yield* context.events.publish(
440
+ Info.make({ message: `fixing ${name} — ${issues.length} finding(s)` })
441
+ )
442
+ const relativePath = relOf.get(name)
443
+ if (relativePath !== undefined) {
444
+ yield* turn(
445
+ programFixAsk(name, relativePath, issues),
446
+ `modernize(${pack.name}): gate fixes ${name}`
447
+ )
448
+ }
449
+ }
450
+ if (global.length > 0) {
451
+ yield* context.events.publish(
452
+ Info.make({ message: `fixing estate-wide findings — ${global.length}` })
453
+ )
454
+ yield* turn(globalFixAsk(global), `modernize(${pack.name}): gate fixes (estate-wide)`)
455
+ }
456
+ })
457
+
458
+ let result = yield* stage(context.events, "gate", gateEvaluate)
459
+ for (let round = 2; round <= MaxRounds && result.issues.length > 0; round += 1) {
460
+ yield* stage(context.events, `gate fixes (round ${round})`, fixOnce(result))
461
+ result = yield* stage(context.events, `gate (round ${round})`, gateEvaluate)
462
+ }
463
+
464
+ if (result.issues.length === 0) {
465
+ yield* stage(
466
+ context.events,
467
+ "plan",
468
+ Effect.gen(function* () {
469
+ const specTexts: Array<string> = []
470
+ for (const unit of units) {
471
+ const spec = yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))
472
+ if (spec !== undefined) {
473
+ specTexts.push(spec)
474
+ }
475
+ }
476
+ const plan = yield* planFrom(
477
+ context.reasoning,
478
+ capText(specTexts.join("\n\n"), limit),
479
+ `${defaultPlanInstructions}\n\n${pack.prompt("plan") ?? ""}`
480
+ )
481
+ yield* files.writeAtomic(join(modDirAbs, "plan.md"), plan.render)
482
+ })
483
+ )
484
+ }
485
+
486
+ const verdict =
487
+ result.issues.length === 0
488
+ ? "PASSED — pending human approval"
489
+ : `DRAFT — ${result.issues.length} open issue(s)`
490
+ yield* stage(
491
+ context.events,
492
+ "commit",
493
+ files
494
+ .writeAtomic(join(modDirAbs, "README.md"), withDraftApproval(readmeFor(pack, verdict)))
495
+ .pipe(
496
+ Effect.andThen(
497
+ context.git
498
+ .commitAll(`modernize(${pack.name}): spec pack (${verdict})`)
499
+ .pipe(Effect.asVoid)
500
+ )
501
+ )
502
+ )
503
+
504
+ if (result.issues.length > 0) {
505
+ return yield* FlowAborted.make({
506
+ message:
507
+ `extraction gate not cleared after ${MaxRounds} round(s) — spec pack committed as draft:\n` +
508
+ result.issues.map((issue) => `- ${issue.title}`).join("\n")
509
+ })
510
+ }
511
+ yield* context.events.publish(
512
+ Info.make({
513
+ message:
514
+ `spec pack ready — review ${ModDir}/README.md, set '- [x] Approved', ` +
515
+ "then run the seed phase"
516
+ })
517
+ )
518
+ })
519
+ )
520
+ })
521
+
522
+ runFlowMain(program)
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,10 +25,6 @@
25
25
  "./Package": "./dist/Package.js",
26
26
  "./package.json": "./package.json"
27
27
  },
28
- "scripts": {
29
- "clean": "tsc -b tsconfig.json --clean",
30
- "test": "vitest run test"
31
- },
32
28
  "publishConfig": {
33
29
  "access": "public",
34
30
  "provenance": true
@@ -53,14 +49,18 @@
53
49
  ],
54
50
  "dependencies": {
55
51
  "@effect/platform-node": "^4.0.0-beta.102",
56
- "@llm4ts/core": "workspace:*",
57
- "@llm4ts/flow": "workspace:*",
58
- "@llm4ts/runner": "workspace:*"
52
+ "@llm4ts/core": "0.2.2",
53
+ "@llm4ts/flow": "0.2.2",
54
+ "@llm4ts/runner": "0.2.2"
59
55
  },
60
56
  "peerDependencies": {
61
57
  "effect": "^4.0.0-beta.102"
62
58
  },
63
59
  "devDependencies": {
64
60
  "effect": "^4.0.0-beta.102"
61
+ },
62
+ "scripts": {
63
+ "clean": "tsc -b tsconfig.json --clean",
64
+ "test": "vitest run test"
65
65
  }
66
- }
66
+ }