@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.
- package/flows/lib/port.js +147 -0
- package/flows/port-compile.js +139 -0
- package/flows/port-files.js +173 -0
- package/flows/port-guide.js +245 -0
- package/flows/port-ledger.js +180 -0
- package/flows/port-tests.js +177 -0
- package/kits/port/README.md +32 -0
- package/kits/port/packs/scala-ts/pack.md +36 -0
- package/kits/port/packs/scala-ts/patterns/pitfalls-scala-ts.md +21 -0
- package/kits/port/packs/scala-ts/prompts/porting.md +64 -0
- package/kits/port/packs/scala-ts/reviewers/effect-fidelity.md +13 -0
- package/kits/port/packs/zig-rust/pack.md +43 -0
- package/kits/port/packs/zig-rust/patterns/pitfalls-zig-rust.md +22 -0
- package/kits/port/packs/zig-rust/prompts/porting.md +67 -0
- package/kits/port/packs/zig-rust/reviewers/port-fidelity.md +13 -0
- package/package.json +4 -4
|
@@ -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);
|