@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.
- package/dist/Cli.d.ts +2 -2
- package/dist/Cli.js +3 -3
- package/dist/Cli.js.map +1 -1
- package/dist/FlowCatalog.d.ts.map +1 -1
- package/dist/FlowCatalog.js +6 -2
- package/dist/FlowCatalog.js.map +1 -1
- package/flows/implement.js +29 -0
- package/flows/issue-pr.js +71 -0
- package/flows/judge-suite.js +48 -0
- package/flows/local.js +40 -0
- package/flows/modernize-bench.js +264 -0
- package/flows/modernize-extract.js +455 -0
- package/flows/modernize-implement.js +249 -0
- package/flows/modernize-review.js +237 -0
- package/flows/modernize-seed.js +189 -0
- package/flows/modernize-survey.js +0 -0
- package/flows/modernize-verify.js +435 -0
- package/flows/sdd.js +114 -0
- package/package.json +5 -5
- package/src/Cli.ts +3 -3
- package/src/FlowCatalog.ts +8 -2
- package/flows/implement.ts +0 -38
- package/flows/issue-pr.ts +0 -108
- package/flows/judge-suite.ts +0 -63
- package/flows/local.ts +0 -59
- package/flows/modernize-bench.ts +0 -368
- package/flows/modernize-extract.ts +0 -642
- package/flows/modernize-implement.ts +0 -340
- package/flows/modernize-review.ts +0 -351
- package/flows/modernize-seed.ts +0 -266
- package/flows/modernize-survey.ts +0 -0
- package/flows/modernize-verify.ts +0 -618
- package/flows/sdd.ts +0 -181
package/flows/modernize-seed.ts
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
// Legacy modernization phase 2: seed the target repository from the APPROVED spec pack.
|
|
2
|
-
//
|
|
3
|
-
// Runs rooted at the TARGET repository (`--repo <target>`), reading the spec
|
|
4
|
-
// pack out of the legacy repository (`LLM4TS_LEGACY_REPO`). Deliberately
|
|
5
|
-
// deterministic — no LLM calls:
|
|
6
|
-
//
|
|
7
|
-
// 1. Refuses to run until a human has flipped `- [x] Approved` in the legacy
|
|
8
|
-
// repo's docs/modernization/README.md (extract writes it unchecked).
|
|
9
|
-
// 2. An empty target gets the pack's scaffold; a non-empty one is used as-is.
|
|
10
|
-
// 3. Specs, traceability, mapping, and rules.txt land in the pack's
|
|
11
|
-
// specs-dir; .feature files land in its features-dir. Legacy SOURCE never
|
|
12
|
-
// crosses — that is the clean-room wall the later phases enforce.
|
|
13
|
-
// 4. The proposed plan is re-parsed (a hard validation) and materialized at
|
|
14
|
-
// docs/modernization/plan.md for modernize-implement to resume.
|
|
15
|
-
// 5. The provenance manifest (docs/modernization/provenance.json) records the
|
|
16
|
-
// clean-room receipt: spec file hashes, gate verdict digests, the pack,
|
|
17
|
-
// the llm4ts version, the extraction seats, and LLM4TS_APPROVER when set.
|
|
18
|
-
//
|
|
19
|
-
// Run: LLM4TS_LEGACY_REPO=~/estates/meridian-legacy \
|
|
20
|
-
// modernize-seed --repo ~/services/meridian-transfers
|
|
21
|
-
import { join } from "node:path"
|
|
22
|
-
import * as Effect from "effect/Effect"
|
|
23
|
-
import * as Schema from "effect/Schema"
|
|
24
|
-
import { FlowAborted, PersistenceError } from "@llm4ts/flow/FlowError"
|
|
25
|
-
import { Info } from "@llm4ts/flow/FlowEvents"
|
|
26
|
-
import { packageVersion } from "@llm4ts/flow/Package"
|
|
27
|
-
import { parsePlan } from "@llm4ts/flow/Plan"
|
|
28
|
-
import { stage } from "@llm4ts/flow/PlanExecution"
|
|
29
|
-
import { Provenance, makeProvenanceStore } from "@llm4ts/flow/Provenance"
|
|
30
|
-
import { matchingFiles } from "@llm4ts/flow/SpecChecks"
|
|
31
|
-
import type { WorkspaceShape } from "@llm4ts/flow/Workspace"
|
|
32
|
-
import { requireApproval } from "@llm4ts/modernize/Approval"
|
|
33
|
-
import { coderFromEnv } from "@llm4ts/runner/Connectors"
|
|
34
|
-
import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
|
|
35
|
-
import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
|
|
36
|
-
import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
|
|
37
|
-
import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
|
|
38
|
-
import { openPack } from "@llm4ts/runner/Packs"
|
|
39
|
-
|
|
40
|
-
const ModDir = "docs/modernization"
|
|
41
|
-
const skipped = new Set([".git", "target", "node_modules", "dist"])
|
|
42
|
-
|
|
43
|
-
const Seats = Schema.Record(Schema.String, Schema.String)
|
|
44
|
-
|
|
45
|
-
const isLitter = (path: string): boolean => path.split("/").some((segment) => skipped.has(segment))
|
|
46
|
-
|
|
47
|
-
/** Copies every text file under `from` into `into`, skipping build and VCS litter. */
|
|
48
|
-
const copyTree = Effect.fn("modernize-seed.copyTree")(function* (
|
|
49
|
-
source: WorkspaceShape,
|
|
50
|
-
target: WorkspaceShape,
|
|
51
|
-
from: string,
|
|
52
|
-
into: string
|
|
53
|
-
) {
|
|
54
|
-
const paths = yield* source.discover(`${from}/**`).pipe(Effect.orElseSucceed(() => []))
|
|
55
|
-
let copied = 0
|
|
56
|
-
for (const path of paths) {
|
|
57
|
-
if (isLitter(path)) {
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
const contents = yield* source.read(path).pipe(Effect.orElseSucceed(() => undefined))
|
|
61
|
-
if (contents === undefined) {
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
yield* target.write(join(into, path.slice(from.length + 1)), contents)
|
|
65
|
-
copied += 1
|
|
66
|
-
}
|
|
67
|
-
return copied
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
/** Copies one file when it exists; false when the source is absent. */
|
|
71
|
-
const copyFile = Effect.fn("modernize-seed.copyFile")(function* (
|
|
72
|
-
source: WorkspaceShape,
|
|
73
|
-
target: WorkspaceShape,
|
|
74
|
-
from: string,
|
|
75
|
-
into: string
|
|
76
|
-
) {
|
|
77
|
-
const contents = yield* source.read(from).pipe(Effect.orElseSucceed(() => undefined))
|
|
78
|
-
if (contents === undefined) {
|
|
79
|
-
return false
|
|
80
|
-
}
|
|
81
|
-
yield* target.write(into, contents)
|
|
82
|
-
return true
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
const program = Effect.gen(function* () {
|
|
86
|
-
const input = yield* resolveFlowInput("Seed the target repository from the approved spec pack")
|
|
87
|
-
const legacyRepo = process.env.LLM4TS_LEGACY_REPO?.trim()
|
|
88
|
-
const files = nodePlainFileStore
|
|
89
|
-
|
|
90
|
-
yield* runNode(
|
|
91
|
-
{
|
|
92
|
-
workDir: input.workDir,
|
|
93
|
-
workspace: input.workspace,
|
|
94
|
-
userPrompt: input.prompt,
|
|
95
|
-
// This phase makes no model call; the seat is wired only because the
|
|
96
|
-
// runner composes one context shape for every flow.
|
|
97
|
-
coder: coderFromEnv(process.env),
|
|
98
|
-
environment: process.env
|
|
99
|
-
},
|
|
100
|
-
(context) =>
|
|
101
|
-
Effect.gen(function* () {
|
|
102
|
-
if (legacyRepo === undefined || legacyRepo.length === 0) {
|
|
103
|
-
return yield* FlowAborted.make({
|
|
104
|
-
message:
|
|
105
|
-
"set LLM4TS_LEGACY_REPO to the legacy repository holding the approved spec pack"
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
const legacy = yield* makeNodeWorkspace(legacyRepo)
|
|
109
|
-
const target = yield* makeNodeWorkspace(input.workDir)
|
|
110
|
-
const opened = yield* stage(
|
|
111
|
-
context.events,
|
|
112
|
-
"pack",
|
|
113
|
-
openPack({
|
|
114
|
-
environment: process.env,
|
|
115
|
-
launchDir: input.workspace,
|
|
116
|
-
flowDir: import.meta.dirname
|
|
117
|
-
})
|
|
118
|
-
)
|
|
119
|
-
const pack = opened.pack
|
|
120
|
-
const specPackRoot = join(legacyRepo, ModDir)
|
|
121
|
-
|
|
122
|
-
yield* stage(
|
|
123
|
-
context.events,
|
|
124
|
-
"approval",
|
|
125
|
-
requireApproval(files, join(specPackRoot, "README.md"))
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
yield* stage(
|
|
129
|
-
context.events,
|
|
130
|
-
"scaffold",
|
|
131
|
-
Effect.gen(function* () {
|
|
132
|
-
const existing = (yield* target.discover().pipe(Effect.orElseSucceed(() => []))).filter(
|
|
133
|
-
(path) => !path.startsWith(".git/")
|
|
134
|
-
)
|
|
135
|
-
if (existing.length > 0) {
|
|
136
|
-
yield* context.events.publish(
|
|
137
|
-
Info.make({ message: "target repo is not empty — using it as-is" })
|
|
138
|
-
)
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
if (pack.scaffold === undefined) {
|
|
142
|
-
return yield* FlowAborted.make({
|
|
143
|
-
message: `target repo is empty and pack '${pack.name}' has no scaffold`
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
// The scaffold path is relative to the pack directory.
|
|
147
|
-
const scaffoldRoot = join(pack.dir, pack.scaffold)
|
|
148
|
-
const scaffold = yield* makeNodeWorkspace(join(opened.workspace.root, scaffoldRoot))
|
|
149
|
-
const paths = (yield* scaffold.discover()).filter((path) => !isLitter(path))
|
|
150
|
-
for (const path of paths) {
|
|
151
|
-
yield* target.write(path, yield* scaffold.read(path))
|
|
152
|
-
}
|
|
153
|
-
yield* context.events.publish(
|
|
154
|
-
Info.make({ message: `scaffolded ${paths.length} file(s) from ${scaffoldRoot}` })
|
|
155
|
-
)
|
|
156
|
-
})
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
const seeded = yield* stage(
|
|
160
|
-
context.events,
|
|
161
|
-
"seed",
|
|
162
|
-
Effect.gen(function* () {
|
|
163
|
-
const specs = yield* copyTree(legacy, target, `${ModDir}/specs`, pack.specsDir)
|
|
164
|
-
// A spec pack that contributes no specs means the wrong legacy repo,
|
|
165
|
-
// or an extraction that never wrote any. Seeding an empty target and
|
|
166
|
-
// reporting success would push that discovery minutes downstream.
|
|
167
|
-
if (specs === 0) {
|
|
168
|
-
return yield* FlowAborted.make({
|
|
169
|
-
message:
|
|
170
|
-
`no specs found under ${legacyRepo}/${ModDir}/specs — ` +
|
|
171
|
-
"check LLM4TS_LEGACY_REPO and that extraction wrote its spec pack"
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
const features = yield* copyTree(legacy, target, `${ModDir}/features`, pack.featuresDir)
|
|
175
|
-
for (const index of ["traceability.md", "mapping.md", "rules.txt"]) {
|
|
176
|
-
yield* copyFile(legacy, target, `${ModDir}/${index}`, join(pack.specsDir, index))
|
|
177
|
-
}
|
|
178
|
-
return specs + features
|
|
179
|
-
})
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
const plan = yield* stage(
|
|
183
|
-
context.events,
|
|
184
|
-
"plan",
|
|
185
|
-
Effect.gen(function* () {
|
|
186
|
-
const text = yield* files.read(join(specPackRoot, "plan.md"))
|
|
187
|
-
if (text === undefined) {
|
|
188
|
-
return yield* PersistenceError.make({
|
|
189
|
-
message: "spec pack has no plan.md — did extraction clear its gate?"
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
// Re-parsing is the hard validation: a malformed plan fails here,
|
|
193
|
-
// not minutes into the implementation phase.
|
|
194
|
-
const parsed = yield* parsePlan(text)
|
|
195
|
-
yield* target.write(`${ModDir}/plan.md`, parsed.render)
|
|
196
|
-
return parsed
|
|
197
|
-
})
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
yield* stage(
|
|
201
|
-
context.events,
|
|
202
|
-
"provenance",
|
|
203
|
-
Effect.gen(function* () {
|
|
204
|
-
const provenance = makeProvenanceStore(files)
|
|
205
|
-
const specFiles = yield* matchingFiles(target, `^${pack.specsDir}/.*\\.md$`)
|
|
206
|
-
const specs = yield* provenance.hashFiles(input.workDir, specFiles)
|
|
207
|
-
const gateFiles = yield* matchingFiles(legacy, `^${ModDir}/gate/.*\\.json$`).pipe(
|
|
208
|
-
Effect.orElseSucceed(() => [])
|
|
209
|
-
)
|
|
210
|
-
const verdicts = yield* provenance.hashFiles(legacyRepo, gateFiles)
|
|
211
|
-
const seatsText = yield* files
|
|
212
|
-
.read(join(specPackRoot, "seats.json"))
|
|
213
|
-
.pipe(Effect.orElseSucceed(() => undefined))
|
|
214
|
-
// A pre-seats spec pack, or a malformed sidecar, degrades to no
|
|
215
|
-
// recorded seats rather than failing the seed.
|
|
216
|
-
const seats =
|
|
217
|
-
seatsText === undefined
|
|
218
|
-
? {}
|
|
219
|
-
: yield* Effect.try({
|
|
220
|
-
try: () => Schema.decodeUnknownSync(Schema.fromJsonString(Seats))(seatsText),
|
|
221
|
-
catch: () => undefined
|
|
222
|
-
}).pipe(Effect.orElseSucceed((): Readonly<Record<string, string>> => ({})))
|
|
223
|
-
const approver = process.env.LLM4TS_APPROVER?.trim()
|
|
224
|
-
yield* provenance.write(
|
|
225
|
-
join(input.workDir, ModDir, "provenance.json"),
|
|
226
|
-
Provenance.make({
|
|
227
|
-
schema: 1,
|
|
228
|
-
pack: pack.name,
|
|
229
|
-
llm4tsVersion: packageVersion,
|
|
230
|
-
createdAt: new Date().toISOString(),
|
|
231
|
-
...(approver === undefined || approver.length === 0
|
|
232
|
-
? {}
|
|
233
|
-
: { approvedBy: approver }),
|
|
234
|
-
seats,
|
|
235
|
-
specs,
|
|
236
|
-
gateVerdicts: Object.fromEntries(
|
|
237
|
-
Object.entries(verdicts).map(([path, hash]) => [
|
|
238
|
-
(path.split("/").at(-1) ?? path).replace(/\.json$/, ""),
|
|
239
|
-
hash
|
|
240
|
-
])
|
|
241
|
-
),
|
|
242
|
-
fixSpecs: []
|
|
243
|
-
})
|
|
244
|
-
)
|
|
245
|
-
})
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
yield* stage(
|
|
249
|
-
context.events,
|
|
250
|
-
"commit",
|
|
251
|
-
context.git
|
|
252
|
-
.commitAll(
|
|
253
|
-
`modernize(${pack.name}): seed specs, features, plan, and provenance (${seeded} file(s))`
|
|
254
|
-
)
|
|
255
|
-
.pipe(Effect.asVoid)
|
|
256
|
-
)
|
|
257
|
-
yield* context.events.publish(
|
|
258
|
-
Info.make({
|
|
259
|
-
message: `target seeded (plan ${plan.epicId}) — run modernize-implement with the same --repo`
|
|
260
|
-
})
|
|
261
|
-
)
|
|
262
|
-
})
|
|
263
|
-
)
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
runFlowMain(program)
|
|
Binary file
|