@llm4ts/shell 2.32.0 → 2.34.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.
@@ -0,0 +1,147 @@
1
+ // Shared by the port flows (ADR 0028): the pack as a porting pack, the
2
+ // manifest of sources and their targets, the rulebook and pitfall cards
3
+ // the implementer reads, and the prompts. The seats and the queue are the
4
+ // flows' own business.
5
+ import { join } from "node:path";
6
+ import * as Effect from "effect/Effect";
7
+ import { autonomyContract } from "@llm4ts/flow/AutonomyContract";
8
+ import { cap } from "@llm4ts/flow/Context";
9
+ import { FlowAborted } from "@llm4ts/flow/FlowError";
10
+ import { loadPatternCards } from "@llm4ts/flow/Patterns";
11
+ import { PortEntry, linesOf, parseLedger, portStatusInstruction, targetPathOf } from "@llm4ts/flow/Port";
12
+ import { matchingFiles } from "@llm4ts/flow/SpecChecks";
13
+ /** Where the port flows keep their state, under the target repository. */
14
+ export const portStateDir = (workDir) => join(workDir, ".llm4ts", "port");
15
+ export const defaultRulebook = [
16
+ "You translate one source file to its target language, faithfully and mechanically:",
17
+ "the same structure, the same names, the same control flow, so a reviewer can read source",
18
+ "and target side by side. Translate what the source does, never what you think it should",
19
+ "do. Flag anything you cannot translate confidently with a TODO(port): <reason> comment",
20
+ "instead of guessing: a flag is better than wrong code."
21
+ ].join("\n");
22
+ /** The pack read as a porting pack: `target:` is required, the rulebook is `prompts/porting.md`. */
23
+ export const asPortingPack = Effect.fn("port.asPortingPack")(function* (opened) {
24
+ const pack = opened.pack;
25
+ if (pack.target === undefined) {
26
+ return yield* FlowAborted.make({
27
+ message: `pack '${pack.name}' has no 'target:' template (e.g. target: {{dir}}/{{base}}.rs); it is not a porting pack`
28
+ });
29
+ }
30
+ const cards = yield* loadPatternCards(opened.workspace, `${opened.dir}/patterns`);
31
+ const pitfalls = cards
32
+ .filter((card) => card.id.startsWith("pitfalls"))
33
+ .map((card) => `### ${card.id}\n${card.body.trim()}`)
34
+ .join("\n\n");
35
+ return {
36
+ pack,
37
+ target: pack.target,
38
+ comment: pack.comment ?? "//",
39
+ rulebook: pack.prompt("porting") ?? defaultRulebook,
40
+ pitfalls
41
+ };
42
+ });
43
+ /** Every source the pack matches, with its line count and target; sorted by path. */
44
+ export const portManifest = Effect.fn("port.manifest")(function* (repo, files, workDir, porting) {
45
+ const sources = yield* matchingFiles(repo, porting.pack.sources ?? "(?!)", porting.pack.exclude);
46
+ const entries = [];
47
+ for (const source of sources) {
48
+ const text = (yield* files.read(join(workDir, source))) ?? "";
49
+ entries.push(PortEntry.make({
50
+ id: source,
51
+ source,
52
+ target: targetPathOf(porting.target, source),
53
+ loc: linesOf(text)
54
+ }));
55
+ }
56
+ return entries;
57
+ });
58
+ /** The denials that keep an implementer on its one file (the Bun port's prompt, in spirit). */
59
+ export const implementerDenials = [
60
+ "Read exactly one source file: the one named below. Do NOT read other source files for",
61
+ "context: the rulebook's maps are authoritative for cross-file references. Do NOT run a",
62
+ "build, a test or any git command: the flow builds and commits. Write only the target file.",
63
+ "Match the source's structure: same function names, same field order, same control flow."
64
+ ].join("\n");
65
+ export const implementerSystem = (porting) => [
66
+ autonomyContract("minimal"),
67
+ "",
68
+ "# Porting rulebook",
69
+ porting.rulebook,
70
+ ...(porting.pitfalls.length === 0
71
+ ? []
72
+ : ["", "# Pitfalls: syntactically alike, semantically different", porting.pitfalls]),
73
+ "",
74
+ implementerDenials
75
+ ].join("\n");
76
+ export const implementerPrompt = (entry, sourceText, porting, sourceChars, ledgerRows) => [
77
+ `Port \`${entry.source}\` (${entry.loc} lines). Write the ported file at: ${entry.target}`,
78
+ "The draft need not compile; it must capture the logic faithfully.",
79
+ ...(ledgerRows === undefined
80
+ ? []
81
+ : [
82
+ "",
83
+ "Precomputed classifications for this file (cross-file analysis; trust them over local guessing):",
84
+ ledgerRows
85
+ ]),
86
+ "",
87
+ portStatusInstruction(porting.comment),
88
+ "",
89
+ `Source file \`${entry.source}\`:`,
90
+ "```",
91
+ cap(sourceText, sourceChars).text,
92
+ "```"
93
+ ].join("\n");
94
+ /** The fixer's brief for a drafted file: apply findings against the source, nothing else. */
95
+ export const portFixerSystem = (porting) => [
96
+ autonomyContract("minimal"),
97
+ "",
98
+ "You apply review findings to a ported file another coder drafted. Apply the findings and",
99
+ "nothing else; where a finding conflicts with the source file's behaviour, the source wins.",
100
+ "Keep the PORT STATUS trailer at the end and update its confidence and todos honestly.",
101
+ "If a finding is wrong (the reviewer misread the code), skip it and say so in one line.",
102
+ "",
103
+ "# Porting rulebook",
104
+ porting.rulebook
105
+ ].join("\n");
106
+ /** The compile fixer's brief: one unit, its diagnostics, no build, no git. */
107
+ export const compileFixerSystem = (porting) => [
108
+ autonomyContract("minimal"),
109
+ "",
110
+ "You fix compiler diagnostics in ONE unit of a mechanically ported code base. Edit only the",
111
+ "files the diagnostics name and their unit. Other units are being fixed by other coders at",
112
+ "the same time: a broken import from another unit is EXPECTED, leave it. Do NOT run a build",
113
+ "or any git command: the flow rebuilds once per round and commits. Never stub a function,",
114
+ "weaken a type to `any` or its equivalent, or add a comment explaining why a workaround is",
115
+ "acceptable: if the fix needs a paragraph of justification, the fix is wrong. Where a fix",
116
+ "needs another unit to change, skip it and say which unit in one line.",
117
+ "",
118
+ "# Porting rulebook",
119
+ porting.rulebook
120
+ ].join("\n");
121
+ /** The ledger the pack's `port-ledger` writes, beside the specs; `undefined` when none. */
122
+ export const ledgerPath = (porting) => `${porting.pack.specsDir}/ledger.tsv`;
123
+ export const readLedgerRows = (files, workDir, porting) => Effect.map(files.read(join(workDir, ledgerPath(porting))), (text) => text === undefined ? [] : parseLedger(text));
124
+ /** A native engineer's brief for the trial port: no rulebook, idiomatic target code. */
125
+ export const nativeImplementerSystem = (porting) => [
126
+ autonomyContract("minimal"),
127
+ "",
128
+ `You are a senior engineer in the target language of pack ${porting.pack.name}. Port the`,
129
+ "source file the way you would write it natively and idiomatically, with no rulebook: your",
130
+ "draft is compared with a rules-following draft to find what the rulebook forgot. Read",
131
+ "exactly one source file; do not run a build or any git command; write only the target file.",
132
+ "",
133
+ implementerDenials
134
+ ].join("\n");
135
+ export const portEnv = (environment) => {
136
+ const int = (raw, fallback, min = 1) => {
137
+ const value = Number.parseInt(raw ?? "", 10);
138
+ return Number.isFinite(value) && value >= min ? value : fallback;
139
+ };
140
+ return {
141
+ pilot: int(environment.LLM4TS_PORT_PILOT, 0, 1),
142
+ concurrency: int(environment.LLM4TS_PORT_CONCURRENCY, 4),
143
+ batch: int(environment.LLM4TS_PORT_BATCH, 100),
144
+ votes: int(environment.LLM4TS_REVIEW_VOTES, 2),
145
+ sourceChars: int(environment.LLM4TS_PORT_SOURCE_CHARS, 120_000, 1_000)
146
+ };
147
+ };
@@ -0,0 +1,139 @@
1
+ // Compiler diagnostics as a work queue (ADR 0028, the Bun port's Phase B/D):
2
+ // the pack's `## Diagnostics` command runs once per round, its output is
3
+ // grouped by unit, one fixer per unit edits only that unit (no build, no
4
+ // git), two adversarial votes review, the flow rebuilds and commits. Until
5
+ // dry, or until a round lowers nothing (typed Stalled).
6
+ //
7
+ // LLM4TS_PACK=zig-rust llm4ts run port-compile --repo ~/src/bun
8
+ //
9
+ // Knobs: LLM4TS_PORT_CONCURRENCY (4), LLM4TS_REVIEW_VOTES (2 here),
10
+ // LLM4TS_PORT_COMPILE_ROUNDS (6).
11
+ import { join } from "node:path";
12
+ import * as Effect from "effect/Effect";
13
+ import * as Ref from "effect/Ref";
14
+ import { makeChat } from "@llm4ts/flow/Chat";
15
+ import { groupDiagnostics, parseDiagnostics, renderDiagnostics } from "@llm4ts/flow/Diagnostics";
16
+ import { FlowAborted, ProcessError, Stalled } from "@llm4ts/flow/FlowError";
17
+ import { Info } from "@llm4ts/flow/FlowEvents";
18
+ import { adversarialReviewer, reviewAndFixLoop } from "@llm4ts/flow/Review";
19
+ import { runQueue } from "@llm4ts/flow/WorkQueue";
20
+ import { asReadOnly, coderFromEnv, nodePlainFileStore, nodeProcessExecutor, openPack, resolveFlowInput, runFlowMain, runNode, stage } from "@llm4ts/runner";
21
+ import { asPortingPack, compileFixerSystem, portEnv, portStateDir } from "./lib/port.js";
22
+ const program = Effect.gen(function* () {
23
+ const input = yield* resolveFlowInput("Fix every compiler diagnostic, one unit at a time");
24
+ const coder = coderFromEnv(process.env);
25
+ const files = nodePlainFileStore;
26
+ const knobs = portEnv(process.env);
27
+ const maxRounds = Math.max(1, Number.parseInt(process.env.LLM4TS_PORT_COMPILE_ROUNDS ?? "6", 10) || 6);
28
+ const stateDir = portStateDir(input.workDir);
29
+ yield* runNode({
30
+ workDir: input.workDir,
31
+ workspace: input.workspace,
32
+ userPrompt: input.prompt,
33
+ coder,
34
+ reasoning: asReadOnly(coder),
35
+ reviewers: [asReadOnly(coder)],
36
+ environment: process.env
37
+ }, (context) => Effect.gen(function* () {
38
+ const events = context.events;
39
+ const opened = yield* stage(events, "pack", openPack({
40
+ environment: process.env,
41
+ launchDir: input.workspace,
42
+ flowDir: import.meta.dirname
43
+ }));
44
+ const porting = yield* asPortingPack(opened);
45
+ const diagnostics = porting.pack.diagnostics;
46
+ if (diagnostics === undefined) {
47
+ return yield* FlowAborted.make({
48
+ message: `pack '${porting.pack.name}' has no '## Diagnostics' section (- command: …, - format: json | cargo)`
49
+ });
50
+ }
51
+ const survey = Effect.gen(function* () {
52
+ const ran = yield* nodeProcessExecutor
53
+ .run(diagnostics.command, input.workDir, {})
54
+ .pipe(Effect.mapError((cause) => ProcessError.make({ message: diagnostics.command.join(" "), detail: cause.message })));
55
+ return parseDiagnostics([...ran.stdout, ...ran.stderr].join("\n"), diagnostics.format);
56
+ });
57
+ let previous;
58
+ let flat = 0;
59
+ for (let round = 1; round <= maxRounds; round += 1) {
60
+ const found = yield* stage(events, `diagnostics round ${round}`, survey);
61
+ const groups = groupDiagnostics(found);
62
+ yield* files.writeAtomic(join(stateDir, `diagnostics-${round}.md`), [
63
+ `# Diagnostics, round ${round}`,
64
+ "",
65
+ ...groups.map((group) => `## ${group.unit}\n\n${renderDiagnostics(group.diagnostics, 200)}`)
66
+ ].join("\n"));
67
+ if (found.length === 0) {
68
+ yield* events.publish(Info.make({ message: `compile: clean after ${round - 1} round(s)` }));
69
+ return;
70
+ }
71
+ if (previous !== undefined && found.length >= previous) {
72
+ flat += 1;
73
+ if (flat >= 2) {
74
+ return yield* Stalled.make({
75
+ signal: "no-progress",
76
+ detail: `compile: ${found.length} diagnostic(s) after round ${round - 1}, no lower than the round before`
77
+ });
78
+ }
79
+ }
80
+ else {
81
+ flat = 0;
82
+ }
83
+ previous = found.length;
84
+ yield* events.publish(Info.make({
85
+ message: `compile: round ${round}, ${found.length} diagnostic(s) in ${groups.length} unit(s)`
86
+ }));
87
+ const finished = yield* Ref.make(new Set());
88
+ const units = groups.map((group) => ({
89
+ id: group.unit,
90
+ diagnostics: group.diagnostics
91
+ }));
92
+ yield* runQueue({
93
+ label: `compile round ${round}`,
94
+ items: units,
95
+ done: (unit) => Effect.map(Ref.get(finished), (set) => set.has(unit.id)),
96
+ concurrency: knobs.concurrency,
97
+ maxRounds: 1,
98
+ events,
99
+ ledger: { files, path: join(stateDir, "compile-ledger.jsonl") },
100
+ work: (unit) => Effect.gen(function* () {
101
+ const chat = yield* makeChat(context.coder, {
102
+ system: compileFixerSystem(porting),
103
+ events,
104
+ agent: "coder",
105
+ stall: { repeats: 5 }
106
+ });
107
+ yield* chat.ask([
108
+ `Unit \`${unit.id}\`: ${unit.diagnostics.length} diagnostic(s) from \`${diagnostics.command.join(" ")}\`.`,
109
+ "Fix them in this unit's files. This file is your only runtime evidence; do not rebuild.",
110
+ "",
111
+ renderDiagnostics(unit.diagnostics)
112
+ ].join("\n"));
113
+ yield* reviewAndFixLoop({
114
+ reviewers: [adversarialReviewer, ...porting.pack.lenses],
115
+ reviewerService: context.reviewers[0] ?? context.reasoning,
116
+ coder: chat,
117
+ taskTitle: `compile ${unit.id}`,
118
+ currentDiff: context.git.diffAll,
119
+ events,
120
+ maxRounds: 2,
121
+ votes: knobs.votes
122
+ });
123
+ yield* Ref.update(finished, (set) => new Set([...set, unit.id]));
124
+ return { note: `${unit.diagnostics.length} diagnostic(s) addressed` };
125
+ })
126
+ }).pipe(
127
+ // A unit the fixer could not finish is the next survey's business.
128
+ Effect.catchTag("Stalled", () => Effect.void));
129
+ const dirty = yield* context.git.uncommittedFiles;
130
+ if (dirty.length > 0) {
131
+ yield* context.git.commitAll(`compile: round ${round}, ${units.length} unit(s)`);
132
+ }
133
+ }
134
+ yield* events.publish(Info.make({
135
+ message: `compile: ${maxRounds} round(s) done, diagnostics remain — see ${stateDir}`
136
+ }));
137
+ }));
138
+ });
139
+ runFlowMain(program);
@@ -0,0 +1,173 @@
1
+ // Port a code base file by file (ADR 0028, the Bun port's Phase A): every
2
+ // source the pack matches gets a draft at its target path, written by one
3
+ // implementer that reads the rulebook, the pitfall cards and exactly one
4
+ // source file; reviewed by two adversarial votes; fixed by a separate fixer;
5
+ // ending in a machine-readable PORT STATUS trailer the ledger records. Not
6
+ // clean-room: the source stays in the tree as the spec. Resumable: a target
7
+ // that exists is done.
8
+ //
9
+ // LLM4TS_PACK=zig-rust llm4ts run port-files --repo ~/src/bun
10
+ // LLM4TS_PORT_PILOT=3 … first: three files, a pilot report, an approval
11
+ //
12
+ // Knobs: LLM4TS_PORT_PILOT, LLM4TS_PORT_CONCURRENCY (4), LLM4TS_PORT_BATCH
13
+ // (100), LLM4TS_REVIEW_VOTES (2 here), LLM4TS_PORT_SOURCE_CHARS (120000).
14
+ import { join } from "node:path";
15
+ import * as Clock from "effect/Clock";
16
+ import * as Effect from "effect/Effect";
17
+ import { ApprovedMarker, requireApproval } from "@llm4ts/flow/Approval";
18
+ import { makeChat } from "@llm4ts/flow/Chat";
19
+ import { Info } from "@llm4ts/flow/FlowEvents";
20
+ import { asAddedFileDiff, batchesOf, ledgerRowsFor, portStatusIn, renderPilotReport } from "@llm4ts/flow/Port";
21
+ import { adversarialReviewer, reviewAndFixLoop } from "@llm4ts/flow/Review";
22
+ import { runQueue } from "@llm4ts/flow/WorkQueue";
23
+ import { asReadOnly, coderFromEnv, makeNodeWorkspace, nodePlainFileStore, openPack, resolveFlowInput, runFlowMain, runNode, stage } from "@llm4ts/runner";
24
+ import { asPortingPack, implementerPrompt, implementerSystem, ledgerPath, portEnv, readLedgerRows, portFixerSystem, portManifest, portStateDir } from "./lib/port.js";
25
+ const renderReport = (outcomes, total, pending) => {
26
+ const done = outcomes.filter((outcome) => outcome.status === "done");
27
+ const failed = outcomes.filter((outcome) => outcome.status === "failed");
28
+ const by = (level) => done.filter((outcome) => outcome.confidence === level).length;
29
+ const low = done.filter((outcome) => outcome.confidence === "low");
30
+ return [
31
+ "# Port report",
32
+ "",
33
+ `- sources: ${total}; drafted this run: ${done.length}; failed: ${failed.length}; still pending: ${pending}`,
34
+ `- confidence: ${by("high")} high, ${by("medium")} medium, ${by("low")} low; TODO(port) markers: ${done.reduce((sum, outcome) => sum + (outcome.todos ?? 0), 0)}`,
35
+ "",
36
+ ...(low.length === 0
37
+ ? []
38
+ : [
39
+ "## Re-read against the source first (confidence: low)",
40
+ "",
41
+ ...low.map((outcome) => `- ${outcome.id}${outcome.note === undefined ? "" : ` — ${outcome.note}`}`),
42
+ ""
43
+ ]),
44
+ ...(failed.length === 0
45
+ ? []
46
+ : ["## Failed", "", ...failed.map((outcome) => `- ${outcome.id}: ${outcome.note ?? ""}`), ""])
47
+ ].join("\n");
48
+ };
49
+ const program = Effect.gen(function* () {
50
+ const input = yield* resolveFlowInput("Port every source file the pack matches to its target");
51
+ const coder = coderFromEnv(process.env);
52
+ const files = nodePlainFileStore;
53
+ const knobs = portEnv(process.env);
54
+ const stateDir = portStateDir(input.workDir);
55
+ const pilotPath = join(stateDir, "pilot.md");
56
+ yield* runNode({
57
+ workDir: input.workDir,
58
+ workspace: input.workspace,
59
+ userPrompt: input.prompt,
60
+ coder,
61
+ reasoning: asReadOnly(coder),
62
+ reviewers: [asReadOnly(coder)],
63
+ environment: process.env
64
+ }, (context) => Effect.gen(function* () {
65
+ const events = context.events;
66
+ const repo = yield* makeNodeWorkspace(input.workDir);
67
+ const opened = yield* stage(events, "pack", openPack({
68
+ environment: process.env,
69
+ launchDir: input.workspace,
70
+ flowDir: import.meta.dirname
71
+ }));
72
+ const porting = yield* asPortingPack(opened);
73
+ const manifest = yield* stage(events, "manifest", portManifest(repo, files, input.workDir, porting));
74
+ const exists = (target) => Effect.map(files.read(join(input.workDir, target)), (text) => text !== undefined);
75
+ const pending = [];
76
+ for (const entry of manifest) {
77
+ if (!(yield* exists(entry.target)))
78
+ pending.push(entry);
79
+ }
80
+ yield* events.publish(Info.make({
81
+ message: `port: ${manifest.length} source(s), ${manifest.length - pending.length} already ported, ${pending.length} pending (pack ${porting.pack.name}, target ${porting.target})`
82
+ }));
83
+ if (knobs.pilot === 0 && (yield* files.read(pilotPath)) !== undefined) {
84
+ // A pilot ran: the rest of the port waits for its approval (R7).
85
+ yield* requireApproval(files, pilotPath);
86
+ }
87
+ const items = knobs.pilot > 0 ? pending.slice(0, knobs.pilot) : pending;
88
+ // A ledger written by port-ledger rides into every implementer prompt.
89
+ const ledger = yield* readLedgerRows(files, input.workDir, porting);
90
+ if (ledger.length > 0) {
91
+ yield* events.publish(Info.make({
92
+ message: `port: ${ledger.length} ledger row(s) from ${ledgerPath(porting)}`
93
+ }));
94
+ }
95
+ const batches = batchesOf(items, { files: knobs.batch });
96
+ const started = yield* Clock.currentTimeMillis;
97
+ const outcomes = [];
98
+ for (const [index, batch] of batches.entries()) {
99
+ const report = yield* runQueue({
100
+ label: `port batch ${index + 1}/${batches.length}`,
101
+ items: batch,
102
+ done: (entry) => exists(entry.target),
103
+ concurrency: knobs.concurrency,
104
+ maxRounds: 2,
105
+ events,
106
+ ledger: { files, path: join(stateDir, "ledger.jsonl") },
107
+ afterRound: (round, done) => done.length === 0
108
+ ? Effect.void
109
+ : Effect.asVoid(context.git.commitPaths(`port: ${done.length} file(s) drafted (batch ${index + 1}, round ${round})`, done.map((entry) => entry.target))),
110
+ work: (entry) => Effect.gen(function* () {
111
+ const source = (yield* files.read(join(input.workDir, entry.source))) ?? "";
112
+ const chat = yield* makeChat(context.coder, {
113
+ system: implementerSystem(porting),
114
+ events,
115
+ agent: "coder",
116
+ stall: { repeats: 5 }
117
+ });
118
+ yield* chat.ask(implementerPrompt(entry, source, porting, knobs.sourceChars, ledgerRowsFor(ledger, entry.source)));
119
+ const targetPath = join(input.workDir, entry.target);
120
+ if ((yield* files.read(targetPath)) === undefined) {
121
+ return { note: `nothing written at ${entry.target}` };
122
+ }
123
+ yield* reviewAndFixLoop({
124
+ reviewers: [adversarialReviewer, ...porting.pack.lenses],
125
+ reviewerService: context.reviewers[0] ?? context.reasoning,
126
+ coder: chat,
127
+ taskTitle: `port ${entry.source}`,
128
+ currentDiff: Effect.map(files.read(targetPath), (text) => asAddedFileDiff(entry.target, text ?? "")),
129
+ events,
130
+ maxRounds: 2,
131
+ votes: knobs.votes,
132
+ fixWith: (prompt) => Effect.flatMap(makeChat(context.coder, {
133
+ system: portFixerSystem(porting),
134
+ events,
135
+ agent: "coder",
136
+ stall: { repeats: 5 }
137
+ }), (fixer) => fixer.ask(`${prompt}\n\nThe ported file is ${entry.target}; its source is ${entry.source}.`))
138
+ });
139
+ const status = portStatusIn((yield* files.read(targetPath)) ?? "");
140
+ return {
141
+ ...(status.confidence === undefined ? {} : { confidence: status.confidence }),
142
+ ...(status.todos === undefined ? {} : { todos: status.todos }),
143
+ ...(status.notes === undefined || status.notes === "none"
144
+ ? {}
145
+ : { note: status.notes })
146
+ };
147
+ })
148
+ });
149
+ outcomes.push(...report.outcomes);
150
+ }
151
+ const stillPending = pending.length - outcomes.filter((outcome) => outcome.status === "done").length;
152
+ yield* files.writeAtomic(join(stateDir, "report.md"), renderReport(outcomes, manifest.length, stillPending));
153
+ if (knobs.pilot > 0) {
154
+ const pilot = renderPilotReport({
155
+ outcomes,
156
+ piloted: items.length,
157
+ remaining: pending.length - items.length,
158
+ elapsedMs: (yield* Clock.currentTimeMillis) - started,
159
+ estimatedCostUsd: undefined
160
+ });
161
+ yield* files.writeAtomic(pilotPath, pilot);
162
+ yield* events.publish(Info.make({
163
+ message: `port: pilot of ${items.length} file(s) done — read ${pilotPath}, tick \`${ApprovedMarker}\`, then run again without LLM4TS_PORT_PILOT to port the remaining ${pending.length - items.length}`
164
+ }));
165
+ }
166
+ else {
167
+ yield* events.publish(Info.make({
168
+ message: `port: ${outcomes.filter((o) => o.status === "done").length} drafted, ${stillPending} still pending — report at ${join(stateDir, "report.md")}`
169
+ }));
170
+ }
171
+ }));
172
+ });
173
+ runFlowMain(program);