@llm4ts/shell 0.14.0 → 0.15.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.
@@ -24,8 +24,13 @@
24
24
  // context is bounded by LLM4TS_CONTEXT_BUDGET (chars;
25
25
  // LLM4TS_JUDGE_SOURCES_LIMIT is the deprecated alias). The analyst is bounded
26
26
  // by LLM4TS_ANALYST_TURNS and LLM4TS_MAX_CLOSURE_FILES.
27
+ // LLM4TS_EXTRACT_CONCURRENCY=<n> extracts and judges n programs at once (default
28
+ // 1): the programs of a wave are independent, each gets a commit scoped to its
29
+ // own files, and a failure lets in-flight programs finish before it surfaces.
27
30
  import { join } from "node:path";
28
31
  import * as Effect from "effect/Effect";
32
+ import * as Ref from "effect/Ref";
33
+ import * as Semaphore from "effect/Semaphore";
29
34
  import { Sample } from "@llm4ts/core/eval/Eval";
30
35
  import { judge } from "@llm4ts/core/eval/Judge";
31
36
  import { budget, capped, withShrink } from "@llm4ts/flow/Context";
@@ -42,7 +47,7 @@ import { cachedReview } from "@llm4ts/flow/ReviewCache";
42
47
  import { coverage, coverageUnits, features, matchingFiles } from "@llm4ts/flow/SpecChecks";
43
48
  import { SurveyGraph, closureFor, surveyGraph } from "@llm4ts/flow/Survey";
44
49
  import { withDraftApproval, requireApproval } from "@llm4ts/modernize/Approval";
45
- import { ProgramArtifacts, ProgramUnit, extractProgramsResumably } from "@llm4ts/modernize/Artifacts";
50
+ import { ProgramArtifacts, ProgramUnit, extractProgramsResumably, programArtifactPaths } from "@llm4ts/modernize/Artifacts";
46
51
  import { asReadOnly, coderFromEnv, withTurnLimit } from "@llm4ts/runner/Connectors";
47
52
  import { resolveFlowInput } from "@llm4ts/runner/FlowArgs";
48
53
  import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner";
@@ -63,6 +68,14 @@ const analystTurns = () => positiveEnvInt("LLM4TS_ANALYST_TURNS", 48);
63
68
  * than this gets a bounded, visible subset rather than an unbounded read.
64
69
  */
65
70
  const maxClosureFiles = () => positiveEnvInt("LLM4TS_MAX_CLOSURE_FILES", 40);
71
+ /**
72
+ * Programs extracted (and judged) at once. The programs of a wave are
73
+ * independent — each analyst reads its source and resolved closure, writes
74
+ * its own four files, and gets its own scoped commit — so this divides wall
75
+ * time without changing tokens or cost. Default 1 keeps the sequential
76
+ * narration; the bound is about the coder seat's quota, not correctness.
77
+ */
78
+ const extractConcurrency = () => positiveEnvInt("LLM4TS_EXTRACT_CONCURRENCY", 1);
66
79
  /** `cobol/ACCTXFR.cbl` → `ACCTXFR`: the program name keying every per-program artifact. */
67
80
  const programName = (relativePath) => {
68
81
  const base = relativePath.slice(relativePath.lastIndexOf("/") + 1);
@@ -243,33 +256,44 @@ const program = Effect.gen(function* () {
243
256
  ...(pack.exclude === undefined ? {} : { exclude: pack.exclude })
244
257
  }));
245
258
  // One structured analyst call per program, resumable per program: a rerun
246
- // skips every program whose spec exists, and each program gets its own commit.
259
+ // skips every program whose spec exists, and each program gets its own
260
+ // commit scoped to its four files. LLM4TS_EXTRACT_CONCURRENCY runs that
261
+ // many programs at once; the commit is the one step serialised.
262
+ const concurrency = extractConcurrency();
247
263
  yield* stage(context.events, "extract", Effect.gen(function* () {
248
- for (const [index, unit] of units.entries()) {
249
- const summary = yield* extractProgramsResumably(files, [unit], (target) => Effect.gen(function* () {
264
+ const started = yield* Ref.make(0);
265
+ const commitLock = yield* Semaphore.make(1);
266
+ if (concurrency > 1) {
267
+ yield* context.events.publish(Info.make({
268
+ message: `extracting up to ${concurrency} program(s) at once (LLM4TS_EXTRACT_CONCURRENCY)`
269
+ }));
270
+ }
271
+ const summary = yield* extractProgramsResumably(files, units, (target) => Effect.gen(function* () {
272
+ const ordinal = yield* Ref.updateAndGet(started, (count) => count + 1);
273
+ yield* context.events.publish(Info.make({
274
+ message: `extracting ${target.sourcePath} (${ordinal}/${units.length})`
275
+ }));
276
+ return yield* structuredAndPublish(context.coder, context.events, `${system}\n\n${programAsk(pack, target.sourcePath, closureFor(graph, target.name, maxClosureFiles()))}`, ProgramArtifacts, programArtifactsJsonSchema, "coder").pipe(
277
+ // A turn-limit trip is the wedged-agent tail, not a
278
+ // failure: keep whatever the analyst already produced.
279
+ Effect.catchIf((error) => error.cause?._tag === "TurnLimitError", (error) => Effect.gen(function* () {
280
+ const existing = yield* files.read(join(modDirAbs, "specs", `${target.name}.md`));
281
+ if (existing === undefined) {
282
+ return yield* Effect.fail(error);
283
+ }
250
284
  yield* context.events.publish(Info.make({
251
- message: `extracting ${target.sourcePath} (${index + 1}/${units.length})`
285
+ message: `turn limit hit on ${target.sourcePath} after its spec was written — keeping the work`
252
286
  }));
253
- return yield* structuredAndPublish(context.coder, context.events, `${system}\n\n${programAsk(pack, target.sourcePath, closureFor(graph, target.name, maxClosureFiles()))}`, ProgramArtifacts, programArtifactsJsonSchema, "coder").pipe(
254
- // A turn-limit trip is the wedged-agent tail, not a
255
- // failure: keep whatever the analyst already produced.
256
- Effect.catchIf((error) => error.cause?._tag === "TurnLimitError", (error) => Effect.gen(function* () {
257
- const existing = yield* files.read(join(modDirAbs, "specs", `${target.name}.md`));
258
- if (existing === undefined) {
259
- return yield* Effect.fail(error);
260
- }
261
- yield* context.events.publish(Info.make({
262
- message: `turn limit hit on ${target.sourcePath} after its spec was written — keeping the work`
263
- }));
264
- return ProgramArtifacts.make({
265
- spec: existing,
266
- feature: "",
267
- traceability: "",
268
- mapping: ""
269
- });
270
- })));
271
- }), modDirAbs);
272
- if (summary.created.length > 0) {
287
+ return ProgramArtifacts.make({
288
+ spec: existing,
289
+ feature: "",
290
+ traceability: "",
291
+ mapping: ""
292
+ });
293
+ })));
294
+ }), modDirAbs, {
295
+ concurrency,
296
+ onCreated: (unit) => Effect.gen(function* () {
273
297
  // Tag the traceability fragment with the cards this program's
274
298
  // source matches — regex-decided, so implementation's playbook
275
299
  // is reproducible.
@@ -280,15 +304,18 @@ const program = Effect.gen(function* () {
280
304
  const fragment = (yield* files.read(fragmentPath)) ?? "";
281
305
  yield* files.writeAtomic(fragmentPath, `${fragment.trimEnd()}\n\nPatterns: ${matched.map((card) => card.id).join(", ")}\n`);
282
306
  }
283
- yield* context.git
284
- .commitAll(`modernize(${pack.name}): spec ${unit.name}`)
285
- .pipe(Effect.asVoid);
286
- }
287
- else {
288
- yield* context.events.publish(Info.make({
289
- message: `resume: specs/${unit.name}.md exists — skipping ${unit.sourcePath}`
290
- }));
291
- }
307
+ // Only this program's files, under one permit: a concurrent
308
+ // sibling's half-written artifacts must never ride along.
309
+ yield* commitLock.withPermit(context.git
310
+ .commitPaths(`modernize(${pack.name}): spec ${unit.name}`, programArtifactPaths(unit, ModDir))
311
+ .pipe(Effect.asVoid));
312
+ })
313
+ });
314
+ if (summary.skipped.length > 0) {
315
+ yield* context.events.publish(Info.make({
316
+ message: `resume: ${summary.skipped.length} program(s) already have a spec — skipped ` +
317
+ summary.skipped.join(", ")
318
+ }));
292
319
  }
293
320
  }));
294
321
  // traceability.md / mapping.md are regenerated from the fragments — fixes
@@ -379,10 +406,9 @@ const program = Effect.gen(function* () {
379
406
  });
380
407
  const covered = yield* coverage(repo, pack.coverage, trace);
381
408
  const wellFormed = yield* features(repo, join(ModDir, "features"));
382
- const judged = [];
383
- for (const unit of units) {
384
- judged.push(yield* judgeProgram(unit));
385
- }
409
+ // Verdicts are per program and cached per program, so they judge
410
+ // under the same bound as extraction; the merge is order-stable.
411
+ const judged = yield* Effect.forEach(units, judgeProgram, { concurrency });
386
412
  return mergeReviewResults([covered, wellFormed, docs, ...judged]);
387
413
  });
388
414
  // One bounded fix turn per sub-bar program (own commit) plus one residual
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,10 +49,10 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@effect/platform-node": "4.0.0-beta.102",
52
- "@llm4ts/modernize": "0.14.0",
53
- "@llm4ts/core": "0.14.0",
54
- "@llm4ts/runner": "0.14.0",
55
- "@llm4ts/flow": "0.14.0"
52
+ "@llm4ts/modernize": "0.15.1",
53
+ "@llm4ts/runner": "0.15.1",
54
+ "@llm4ts/core": "0.15.1",
55
+ "@llm4ts/flow": "0.15.1"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "effect": "4.0.0-beta.102"