@llm4ts/shell 0.6.0 → 0.6.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.
@@ -1,340 +0,0 @@
1
- // Legacy modernization phase 3: implement the seeded plan behind the pack's gates.
2
- //
3
- // Runs rooted at the TARGET repository (`--repo <target>`), from the specs
4
- // alone. The clean-room wall is ENFORCED, not advised: the flow refuses to
5
- // start when anything matching the pack's legacy `sources:` regex sits in the
6
- // target workspace, so the coder provably never reads legacy source.
7
- //
8
- // Per plan task: a shared chat implements it, the pack's reviewer lenses plus
9
- // the minimal roster review the diff behind a gate (build for the first,
10
- // tests-encoding task; test for the rest), and the task is committed. The
11
- // first task must leave the acceptance tests RED — tests that pass before any
12
- // implementation encode nothing, so the flow aborts. Pattern cards cited by
13
- // the seeded specs are injected into the coder's brief as an advisory
14
- // translation playbook (the specs still win).
15
- //
16
- // After the loop the verify gate must be green, then a spec-compliance judge
17
- // scores the whole branch against the committed specs and feeds sub-bar
18
- // reasoning back to the coder, bounded by LLM4TS_JUDGE_ROUNDS (default 2).
19
- //
20
- // Run: modernize-implement --repo ~/services/meridian-transfers
21
- import { join } from "node:path"
22
- import * as Effect from "effect/Effect"
23
- import { Dimension, Sample } from "@llm4ts/core/eval/Eval"
24
- import { judge } from "@llm4ts/core/eval/Judge"
25
- import { makeChat } from "@llm4ts/flow/Chat"
26
- import { FlowAborted, FlowLlmError, type FlowError } from "@llm4ts/flow/FlowError"
27
- import { Info } from "@llm4ts/flow/FlowEvents"
28
- import { loadPatternCards, taggedPatternIds } from "@llm4ts/flow/Patterns"
29
- import { makePlanStore } from "@llm4ts/flow/Persistence"
30
- import { implementTaskLoop, stage } from "@llm4ts/flow/PlanExecution"
31
- import {
32
- lintCommand,
33
- minimalReviewers,
34
- reviewAndFixLoop,
35
- type ReviewIssue,
36
- type ReviewResult
37
- } from "@llm4ts/flow/Review"
38
- import { checkWall, wallBreachMessage } from "@llm4ts/flow/Wall"
39
- import type { WorkspaceShape } from "@llm4ts/flow/Workspace"
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 { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
44
- import { nodeProcessExecutor } from "@llm4ts/runner/NodeProcessExecutor"
45
- import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
46
- import { loadUniversalPatternCards, openPack } from "@llm4ts/runner/Packs"
47
-
48
- const ModDir = "docs/modernization"
49
-
50
- const judgeRounds = (): number => {
51
- const raw = Number.parseInt(process.env.LLM4TS_JUDGE_ROUNDS ?? "", 10)
52
- return Number.isFinite(raw) && raw > 0 ? raw : 2
53
- }
54
-
55
- const complianceDimensions = [
56
- Dimension.make({
57
- name: "spec-compliance",
58
- rubric:
59
- "Does the implementation satisfy every rule in the committed specs — exact values, " +
60
- "validation order, error paths — without weakening, deleting, or loosening any test or scenario?"
61
- }),
62
- Dimension.make({
63
- name: "scenario-coverage",
64
- rubric:
65
- "Is every BDD scenario in the seeded feature files exercised by an acceptance test in this diff?"
66
- })
67
- ]
68
-
69
- /** Concatenates the committed specs — the judge's contract text. */
70
- const gatherSpecs = Effect.fn("modernize-implement.gatherSpecs")(function* (
71
- target: WorkspaceShape,
72
- specsDir: string
73
- ) {
74
- const paths = yield* target.discover(`${specsDir}/**`).pipe(Effect.orElseSucceed(() => []))
75
- const parts: Array<string> = []
76
- for (const path of [...paths].sort()) {
77
- const text = yield* target.read(path).pipe(Effect.orElseSucceed(() => ""))
78
- if (text.trim().length > 0) {
79
- parts.push(`===== ${path} =====\n${text}`)
80
- }
81
- }
82
- return parts.join("\n\n")
83
- })
84
-
85
- const issueText = (issues: ReadonlyArray<ReviewIssue>): string =>
86
- issues.map((issue) => `${issue.title}\n${issue.description}`.trim()).join("\n\n")
87
-
88
- const program = Effect.gen(function* () {
89
- const input = yield* resolveFlowInput("Implement the seeded modernization plan")
90
- const coder = coderFromEnv(process.env)
91
- const files = nodePlainFileStore
92
- const planPath = join(input.workDir, ModDir, "plan.md")
93
-
94
- yield* runNode(
95
- {
96
- workDir: input.workDir,
97
- workspace: input.workspace,
98
- userPrompt: input.prompt,
99
- coder,
100
- reasoning: asReadOnly(coder),
101
- reviewers: [asReadOnly(coder)],
102
- environment: process.env
103
- },
104
- (context) =>
105
- Effect.gen(function* () {
106
- const target = yield* makeNodeWorkspace(input.workDir)
107
- const opened = yield* stage(
108
- context.events,
109
- "pack",
110
- openPack({
111
- environment: process.env,
112
- launchDir: input.workspace,
113
- flowDir: import.meta.dirname
114
- })
115
- )
116
- const pack = opened.pack
117
-
118
- yield* stage(
119
- context.events,
120
- "wall",
121
- Effect.gen(function* () {
122
- if (pack.sources === undefined) {
123
- return yield* context.events.publish(
124
- Info.make({ message: "pack has no sources regex — wall check skipped" })
125
- )
126
- }
127
- const result = yield* checkWall(target, pack.sources)
128
- if (result._tag === "Breached") {
129
- return yield* FlowAborted.make({
130
- message: wallBreachMessage(
131
- result,
132
- "The implementation must be driven by the specs alone; remove the files and rerun."
133
- )
134
- })
135
- }
136
- yield* context.events.publish(
137
- Info.make({ message: "clean-room wall: no legacy source in the target workspace" })
138
- )
139
- })
140
- )
141
-
142
- const store = makePlanStore(files)
143
- const plan = yield* store.load(planPath)
144
- if (plan === undefined) {
145
- return yield* FlowAborted.make({
146
- message: `no plan at ${planPath} — run modernize-seed first`
147
- })
148
- }
149
-
150
- const gate = (name: string): Effect.Effect<ReviewResult, FlowError> | undefined => {
151
- const command = pack.gate(name)
152
- return command === undefined
153
- ? undefined
154
- : lintCommand(nodeProcessExecutor, context.events, command, input.workDir)
155
- }
156
- const buildGate = gate("build")
157
- const testGate = gate("test")
158
- const verifyGate = gate("verify") ?? testGate
159
-
160
- yield* stage(
161
- context.events,
162
- "branch",
163
- context.git.checkoutOrCreate(plan.epicId).pipe(Effect.asVoid)
164
- )
165
-
166
- // Pattern selection is deterministic: extraction tagged each program's
167
- // fragment with the cards its SOURCE matched, the specs carry those
168
- // ids, and only the cited cards reach the brief.
169
- const specText = yield* gatherSpecs(target, pack.specsDir)
170
- const cards = [
171
- ...(yield* loadPatternCards(opened.workspace, `${opened.dir}/patterns`)),
172
- ...(yield* loadUniversalPatternCards([input.workspace, import.meta.dirname]))
173
- ]
174
- const cited = new Set(taggedPatternIds(specText))
175
- const playbook = cards.filter((card) => cited.has(card.id))
176
- const system = [
177
- pack.prompt("implement"),
178
- pack.lessons === undefined
179
- ? undefined
180
- : `Lessons from previous modernization runs — apply them:\n${pack.lessons}`,
181
- playbook.length === 0
182
- ? undefined
183
- : "Pattern cards cited by the specs — the translation playbook (advisory, the specs win):\n\n" +
184
- playbook.map((card) => `### ${card.id}\n${card.body}`).join("\n\n")
185
- ]
186
- .filter((part) => part !== undefined)
187
- .join("\n\n")
188
- if (playbook.length > 0) {
189
- yield* context.events.publish(
190
- Info.make({ message: `${playbook.length} pattern card(s) cited by the specs` })
191
- )
192
- }
193
-
194
- const coderChat = yield* makeChat(context.coder, { system })
195
- const firstTitle = plan.tasks[0]?.title
196
-
197
- yield* implementTaskLoop(store, context.events, planPath, plan, (task) =>
198
- Effect.gen(function* () {
199
- const testsTask = task.title === firstTitle
200
- yield* coderChat.ask(plan.taskPrompt(task))
201
- yield* reviewAndFixLoop({
202
- reviewers: [...minimalReviewers, ...pack.lenses],
203
- reviewerService: context.reviewers[0] ?? context.reasoning,
204
- coder: coderChat,
205
- taskTitle: task.title,
206
- currentDiff: context.git.diffAll,
207
- changedFiles: context.git.defaultBase.pipe(
208
- Effect.flatMap((base) => context.git.changedFilesVsBase(base))
209
- ),
210
- events: context.events,
211
- ...(testsTask
212
- ? buildGate === undefined
213
- ? {}
214
- : { lint: buildGate }
215
- : testGate === undefined
216
- ? {}
217
- : { lint: testGate }),
218
- parallelism: 1
219
- })
220
- if (testsTask && testGate !== undefined) {
221
- const red = yield* testGate
222
- if (red.isClean) {
223
- return yield* FlowAborted.make({
224
- message:
225
- "the new acceptance tests pass before any implementation — they encode nothing"
226
- })
227
- }
228
- }
229
- yield* context.git.commitAll(`${plan.epicId}: ${task.title}`).pipe(Effect.asVoid)
230
- })
231
- )
232
-
233
- // The task loop marks each task complete AFTER its per-task commit, so
234
- // the final task's plan update would otherwise be left uncommitted.
235
- // A no-op when the loop already committed everything.
236
- yield* context.git.commitAll(`${plan.epicId}: plan state`).pipe(Effect.asVoid)
237
-
238
- if (verifyGate !== undefined) {
239
- yield* stage(
240
- context.events,
241
- "verify",
242
- Effect.gen(function* () {
243
- const result = yield* verifyGate
244
- if (!result.isClean) {
245
- return yield* FlowAborted.make({
246
- message: `verify gate failed:\n${issueText(result.issues)}`
247
- })
248
- }
249
- })
250
- )
251
- }
252
-
253
- // The branch-level judge: bounded rounds of feedback, each re-gated and
254
- // committed, failing the flow if the bar is never cleared.
255
- yield* stage(
256
- context.events,
257
- "judge",
258
- Effect.gen(function* () {
259
- const contractText = yield* gatherSpecs(target, pack.specsDir)
260
- const complianceJudge = judge(context.reasoning, complianceDimensions)
261
- const rounds = judgeRounds()
262
- for (let round = 1; round <= rounds; round += 1) {
263
- const base = yield* context.git.defaultBase
264
- const diff = yield* context.git.diffVsBase(base)
265
- const scored = yield* complianceJudge
266
- .evaluate(
267
- Sample.make({
268
- response: diff,
269
- context: contractText,
270
- query: input.prompt
271
- })
272
- )
273
- .pipe(Effect.mapError(FlowLlmError.from))
274
- const below = scored.scores.filter((score) => {
275
- const max = complianceDimensions.find((d) => d.name === score.name)?.maxScore ?? 2
276
- return score.score < max
277
- })
278
- if (below.length === 0) {
279
- return yield* context.events.publish(
280
- Info.make({ message: "spec-compliance judge: branch cleared the bar" })
281
- )
282
- }
283
- if (round >= rounds) {
284
- return yield* FlowAborted.make({
285
- message:
286
- `spec-compliance judge not cleared after ${rounds} round(s):\n` +
287
- below.map((d) => `- ${d.name} ${d.score}: ${d.reasoning}`).join("\n")
288
- })
289
- }
290
- yield* coderChat.ask(
291
- [
292
- "The final spec-compliance review scored the branch below the bar. Close these gaps",
293
- "without weakening any test, then stop:",
294
- ...below.map((d) => `- ${d.name} (${d.score}): ${d.reasoning}`)
295
- ].join("\n")
296
- )
297
- if (verifyGate !== undefined) {
298
- const regated = yield* verifyGate
299
- if (!regated.isClean) {
300
- return yield* FlowAborted.make({
301
- message: "verify gate broke while addressing judge feedback"
302
- })
303
- }
304
- }
305
- yield* context.git
306
- .commitAll(`${plan.epicId}: address spec-compliance feedback`)
307
- .pipe(Effect.asVoid)
308
- }
309
- })
310
- )
311
-
312
- // Publishing is best-effort: a repository with no remote or forge is a
313
- // normal local run, not a failure.
314
- yield* stage(
315
- context.events,
316
- "publish",
317
- Effect.gen(function* () {
318
- const base = yield* context.git.defaultBase
319
- yield* context.git.push("origin", plan.epicId)
320
- const pr = yield* context.hosting.createPr(
321
- `modernize: ${plan.epicId}`,
322
- `Implements the approved spec pack. Plan: ${ModDir}/plan.md — all gates green.`,
323
- base
324
- )
325
- yield* context.events.publish(Info.make({ message: `PR: ${pr.url}` }))
326
- }).pipe(
327
- Effect.catch((error) =>
328
- context.events.publish(
329
- Info.make({
330
- message: `publish skipped (no remote/forge configured): ${error.message}`
331
- })
332
- )
333
- )
334
- )
335
- )
336
- })
337
- )
338
- })
339
-
340
- runFlowMain(program)
@@ -1,351 +0,0 @@
1
- // Legacy modernization phase 5: review the increment against its spec pack and distil lessons.
2
- //
3
- // Runs rooted at the TARGET repository (`--repo <target>`) behind the enforced
4
- // clean-room wall — the review judges spec-driven work only.
5
- //
6
- // 1. The full reviewer roster plus the pack's own lenses (filtered to the
7
- // files this branch touched) review the diff against the committed specs.
8
- // 2. A spec-compliance judge scores the branch on the same two dimensions
9
- // modernize-implement gates on.
10
- // 3. Findings and scores are distilled into: fixes (spec VIOLATIONS, which
11
- // become fix specs plus plan tasks for another implement pass),
12
- // improvements (worthwhile but not violations), and lessons — rules of
13
- // thumb that generalize, appended to the pack's lessons.md so the next
14
- // estate starts smarter.
15
- //
16
- // Run: modernize-review --repo ~/services/meridian-transfers
17
- import { join } from "node:path"
18
- import * as Effect from "effect/Effect"
19
- import * as Schema from "effect/Schema"
20
- import { Dimension, Sample, type EvalResult } from "@llm4ts/core/eval/Eval"
21
- import { judge } from "@llm4ts/core/eval/Judge"
22
- import type { JsonSchema } from "@llm4ts/core/Models"
23
- import { FlowAborted, FlowLlmError } from "@llm4ts/flow/FlowError"
24
- import { Info } from "@llm4ts/flow/FlowEvents"
25
- import { appendPackLesson, type Pack } from "@llm4ts/flow/Pack"
26
- import { makePlanStore } from "@llm4ts/flow/Persistence"
27
- import { Plan, Task } from "@llm4ts/flow/Plan"
28
- import { stage } from "@llm4ts/flow/PlanExecution"
29
- import {
30
- allReviewers,
31
- mergeReviewResults,
32
- reviewJsonSchema,
33
- reviewPrompt,
34
- ReviewResult
35
- } from "@llm4ts/flow/Review"
36
- import { checkWall, wallBreachMessage } from "@llm4ts/flow/Wall"
37
- import type { WorkspaceShape } from "@llm4ts/flow/Workspace"
38
- import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors"
39
- import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
40
- import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
41
- import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
42
- import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
43
- import { openPack } from "@llm4ts/runner/Packs"
44
-
45
- const ModDir = "docs/modernization"
46
-
47
- class FixSpec extends Schema.Class<FixSpec>("FixSpec")({
48
- title: Schema.String,
49
- spec: Schema.String,
50
- taskTitle: Schema.String,
51
- taskDescription: Schema.String
52
- }) {}
53
-
54
- class ReviewOutcome extends Schema.Class<ReviewOutcome>("ReviewOutcome")({
55
- fixes: Schema.Array(FixSpec),
56
- improvements: Schema.Array(FixSpec),
57
- lessons: Schema.Array(Schema.String)
58
- }) {}
59
-
60
- const fixSpecJsonSchema = {
61
- type: "object",
62
- properties: {
63
- title: { type: "string" },
64
- spec: { type: "string" },
65
- taskTitle: { type: "string" },
66
- taskDescription: { type: "string" }
67
- },
68
- required: ["title", "spec", "taskTitle", "taskDescription"]
69
- } as const
70
-
71
- const reviewOutcomeJsonSchema: JsonSchema = {
72
- type: "object",
73
- properties: {
74
- fixes: { type: "array", items: { ...fixSpecJsonSchema } },
75
- improvements: { type: "array", items: { ...fixSpecJsonSchema } },
76
- lessons: { type: "array", items: { type: "string" } }
77
- },
78
- required: ["fixes", "improvements", "lessons"]
79
- }
80
-
81
- const complianceDimensions = [
82
- Dimension.make({
83
- name: "spec-compliance",
84
- rubric:
85
- "Does the implementation satisfy every rule in the committed specs — exact values, " +
86
- "validation order, error paths — without weakening, deleting, or loosening any test or scenario?"
87
- }),
88
- Dimension.make({
89
- name: "scenario-coverage",
90
- rubric:
91
- "Is every BDD scenario in the seeded feature files exercised by an acceptance test on this branch?"
92
- })
93
- ]
94
-
95
- const slug = (title: string): string =>
96
- title
97
- .toLowerCase()
98
- .replace(/[^a-z0-9]+/g, "-")
99
- .replace(/^-|-$/g, "")
100
- .slice(0, 60)
101
-
102
- const gatherDir = Effect.fn("modernize-review.gatherDir")(function* (
103
- workspace: WorkspaceShape,
104
- directory: string
105
- ) {
106
- const paths = yield* workspace.discover(`${directory}/**`).pipe(Effect.orElseSucceed(() => []))
107
- const parts: Array<string> = []
108
- for (const path of [...paths].sort()) {
109
- const text = yield* workspace.read(path).pipe(Effect.orElseSucceed(() => ""))
110
- if (text.trim().length > 0) {
111
- parts.push(`===== ${path} =====\n${text}`)
112
- }
113
- }
114
- return parts.join("\n\n")
115
- })
116
-
117
- const distillPrompt = (
118
- pack: Pack,
119
- findings: ReviewResult,
120
- scored: EvalResult,
121
- diff: string
122
- ): string =>
123
- [
124
- pack.prompt("review") ?? "",
125
- "",
126
- "Below are the raw reviewer findings and judge scores for a modernization increment.",
127
- "Distill them:",
128
- '- "fixes": findings where the implementation VIOLATES the committed specs. Each gets a',
129
- " short spec document (Markdown: what is wrong, the spec rule it violates, the expected",
130
- " behaviour) and a plan task (title + description naming the spec rules/scenarios).",
131
- '- "improvements": worthwhile follow-ups that do NOT violate the specs.',
132
- '- "lessons": rules of thumb that would help FUTURE modernizations of this kind — phrased',
133
- " generally (no file paths from this repo), one sentence each. Only include lessons that",
134
- " generalize; an empty list is a fine answer.",
135
- "",
136
- "Reviewer findings:",
137
- findings.issues
138
- .map((issue) => `- [${issue.severity}] ${issue.title}: ${issue.description}`)
139
- .join("\n"),
140
- "",
141
- "Judge scores:",
142
- scored.scores.map((score) => `- ${score.name}: ${score.score} — ${score.reasoning}`).join("\n"),
143
- "",
144
- "Diff under review:",
145
- diff
146
- ].join("\n")
147
-
148
- const program = Effect.gen(function* () {
149
- const input = yield* resolveFlowInput("Review the modernization increment against its spec pack")
150
- const coder = coderFromEnv(process.env)
151
- const files = nodePlainFileStore
152
- const planPath = join(input.workDir, ModDir, "plan.md")
153
-
154
- yield* runNode(
155
- {
156
- workDir: input.workDir,
157
- workspace: input.workspace,
158
- userPrompt: input.prompt,
159
- coder,
160
- reasoning: asReadOnly(coder),
161
- reviewers: [asReadOnly(coder)],
162
- environment: process.env
163
- },
164
- (context) =>
165
- Effect.gen(function* () {
166
- const target = yield* makeNodeWorkspace(input.workDir)
167
- const opened = yield* stage(
168
- context.events,
169
- "pack",
170
- openPack({
171
- environment: process.env,
172
- launchDir: input.workspace,
173
- flowDir: import.meta.dirname
174
- })
175
- )
176
- const pack = opened.pack
177
- const reviewService = context.reviewers[0] ?? context.reasoning
178
-
179
- yield* stage(
180
- context.events,
181
- "wall",
182
- Effect.gen(function* () {
183
- if (pack.sources === undefined) {
184
- return yield* context.events.publish(
185
- Info.make({ message: "pack has no sources regex — wall check skipped" })
186
- )
187
- }
188
- const result = yield* checkWall(target, pack.sources)
189
- if (result._tag === "Breached") {
190
- return yield* FlowAborted.make({
191
- message: wallBreachMessage(
192
- result,
193
- "The review must judge spec-driven work only; remove the files and rerun."
194
- )
195
- })
196
- }
197
- yield* context.events.publish(
198
- Info.make({ message: "clean-room wall: no legacy source in the target workspace" })
199
- )
200
- })
201
- )
202
-
203
- const base = yield* context.git.defaultBase
204
- const diff = yield* context.git.diffVsBase(base)
205
- if (diff.trim().length === 0) {
206
- return yield* FlowAborted.make({
207
- message: `nothing to review: no diff vs ${base} on this branch`
208
- })
209
- }
210
- const changedFiles = yield* context.git.changedFilesVsBase(base)
211
- const specText = yield* gatherDir(target, pack.specsDir)
212
-
213
- const findings = yield* stage(
214
- context.events,
215
- "review",
216
- Effect.gen(function* () {
217
- // Sequential on purpose: free provider tiers rate-limit concurrent
218
- // reviewers, and the roster is small.
219
- const roster = [...allReviewers, ...pack.lenses].filter((lens) =>
220
- lens.matches(changedFiles)
221
- )
222
- const results: Array<ReviewResult> = []
223
- for (const lens of roster) {
224
- const prompt = `${lens.systemPrompt}\n\n${reviewPrompt(
225
- `modernization increment vs committed specs\n\n${specText}`,
226
- diff
227
- )}`
228
- results.push(
229
- yield* reviewService
230
- .executeStructured(prompt, ReviewResult, reviewJsonSchema)
231
- .pipe(Effect.mapError(FlowLlmError.from))
232
- )
233
- }
234
- return mergeReviewResults(results)
235
- })
236
- )
237
-
238
- const scored = yield* stage(
239
- context.events,
240
- "judge",
241
- judge(context.reasoning, complianceDimensions)
242
- .evaluate(Sample.make({ response: diff, context: specText, query: input.prompt }))
243
- .pipe(Effect.mapError(FlowLlmError.from))
244
- )
245
-
246
- const outcome = yield* stage(
247
- context.events,
248
- "distill",
249
- context.reasoning
250
- .executeStructured(
251
- distillPrompt(pack, findings, scored, diff),
252
- ReviewOutcome,
253
- reviewOutcomeJsonSchema
254
- )
255
- .pipe(Effect.mapError(FlowLlmError.from))
256
- )
257
-
258
- yield* stage(
259
- context.events,
260
- "fix specs",
261
- Effect.gen(function* () {
262
- const documents: ReadonlyArray<readonly [string, FixSpec]> = [
263
- ...outcome.fixes.map((fix) => ["fix", fix] as const),
264
- ...outcome.improvements.map((fix) => ["improvement", fix] as const)
265
- ]
266
- for (const [kind, fix] of documents) {
267
- yield* files.writeAtomic(
268
- join(input.workDir, pack.specsDir, "fixes", `${kind}-${slug(fix.title)}.md`),
269
- `# ${fix.title}\n\n${fix.spec}\n`
270
- )
271
- }
272
- if (outcome.fixes.length === 0) {
273
- return yield* context.events.publish(
274
- Info.make({ message: "no spec violations — no plan increment" })
275
- )
276
- }
277
- const store = makePlanStore(files)
278
- const plan = yield* store.load(planPath)
279
- if (plan === undefined) {
280
- return yield* FlowAborted.make({
281
- message: `no plan at ${planPath} — run modernize-seed first`
282
- })
283
- }
284
- yield* store.save(
285
- planPath,
286
- Plan.make({
287
- ...plan,
288
- tasks: [
289
- ...plan.tasks,
290
- ...outcome.fixes.map((fix) =>
291
- Task.make({
292
- title: fix.taskTitle,
293
- description: fix.taskDescription,
294
- completed: false
295
- })
296
- )
297
- ]
298
- })
299
- )
300
- yield* context.events.publish(
301
- Info.make({
302
- message: `${outcome.fixes.length} fix task(s) appended — rerun modernize-implement`
303
- })
304
- )
305
- })
306
- )
307
-
308
- yield* stage(
309
- context.events,
310
- "lessons",
311
- Effect.gen(function* () {
312
- if (outcome.lessons.length === 0) {
313
- return yield* context.events.publish(
314
- Info.make({ message: "no generalizable lessons this round" })
315
- )
316
- }
317
- for (const lesson of outcome.lessons) {
318
- yield* appendPackLesson(opened.workspace, opened.dir, lesson)
319
- }
320
- yield* context.events.publish(
321
- Info.make({
322
- message:
323
- `${outcome.lessons.length} lesson(s) appended to ${opened.dir}/lessons.md — ` +
324
- "review and commit the pack change"
325
- })
326
- )
327
- })
328
- )
329
-
330
- yield* stage(
331
- context.events,
332
- "commit",
333
- context.git
334
- .commitAll(
335
- `modernize(${pack.name}): review — ${outcome.fixes.length} fix(es), ` +
336
- `${outcome.improvements.length} improvement(s)`
337
- )
338
- .pipe(Effect.asVoid)
339
- )
340
- yield* context.events.publish(
341
- Info.make({
342
- message:
343
- `fixes=${outcome.fixes.length} improvements=${outcome.improvements.length} ` +
344
- `lessons=${outcome.lessons.length}`
345
- })
346
- )
347
- })
348
- )
349
- })
350
-
351
- runFlowMain(program)