@glrs-dev/cli 2.4.1 → 2.6.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/CHANGELOG.md +34 -0
- package/dist/{chunk-HQUCVJ4G.js → chunk-FBXSGZAA.js} +4 -0
- package/dist/chunk-J3FXSHMA.js +263 -0
- package/dist/{chunk-5ZVUFNCP.js → chunk-S6N5E2GG.js} +8 -1
- package/dist/{chunk-2VMFXAJH.js → chunk-UO7WHIKY.js} +18 -5
- package/dist/cli.js +10 -3
- package/dist/commands/autopilot-tui.d.ts +11 -1
- package/dist/commands/autopilot-tui.js +2 -1
- package/dist/commands/autopilot.d.ts +2 -0
- package/dist/commands/autopilot.js +62 -21
- package/dist/commands/debrief.d.ts +2 -0
- package/dist/commands/debrief.js +1 -1
- package/dist/commands/loop.d.ts +2 -0
- package/dist/commands/loop.js +33 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.d.ts +9 -0
- package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.js +33 -15
- package/dist/node_modules/@glrs-dev/adapter-opencode/package.json +1 -1
- package/dist/node_modules/@glrs-dev/autopilot/dist/auto-ship-EVLBKHUZ.js +7 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/{changeset-generator-DG3MVWVV.js → changeset-generator-HAHYSSUR.js} +2 -2
- package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-VITL2Z45.js → chunk-2X3CWH47.js} +578 -62
- package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-Q4ULU6ER.js → chunk-2ZQ6SBV3.js} +4 -2
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-6JZQLIRP.js +781 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-E7PWTRFO.js → chunk-AWRK6S6G.js} +2 -2
- package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-M2ZVBPWL.js → chunk-BLEIZHET.js} +1 -1
- package/dist/node_modules/@glrs-dev/autopilot/dist/{chunk-7OSEI5TF.js → chunk-GXXCEGDD.js} +3 -1
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-S34HOCZ4.js +44 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/index.d.ts +159 -9
- package/dist/node_modules/@glrs-dev/autopilot/dist/index.js +115 -35
- package/dist/node_modules/@glrs-dev/autopilot/dist/{logger-UITJGIZE.js → logger-3XLFMXLN.js} +1 -1
- package/dist/node_modules/@glrs-dev/autopilot/dist/loop-session-YLCVJGPV.js +9 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/plan-enrichment-4SQYV5FC.js +17 -0
- package/dist/node_modules/@glrs-dev/autopilot/package.json +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/agents-md-writer.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/architecture-advisor.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-searcher.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/docs-maintainer.md +0 -8
- package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +1 -3
- package/dist/vendor/harness-opencode/dist/agents/prompts/lib-reader.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +0 -2
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +78 -262
- package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +5 -14
- package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +7 -2
- package/dist/vendor/harness-opencode/dist/autopilot/strategies/default.md +29 -0
- package/dist/vendor/harness-opencode/dist/index.js +112 -82
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +1 -1
- package/dist/node_modules/@glrs-dev/autopilot/dist/auto-ship-LCT6LIH7.js +0 -7
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-ZNJWARTM.js +0 -449
- package/dist/node_modules/@glrs-dev/autopilot/dist/loop-session-XKL3NHUA.js +0 -8
- package/dist/node_modules/@glrs-dev/autopilot/dist/plan-enrichment-D3RPJR2J.js +0 -14
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveModel
|
|
3
|
+
} from "./chunk-S34HOCZ4.js";
|
|
4
|
+
import {
|
|
5
|
+
childLogger
|
|
6
|
+
} from "./chunk-2ZQ6SBV3.js";
|
|
7
|
+
import {
|
|
8
|
+
detectSpecPhases,
|
|
9
|
+
hasSpec,
|
|
10
|
+
parseSpecItems
|
|
11
|
+
} from "./chunk-GXXCEGDD.js";
|
|
12
|
+
|
|
13
|
+
// src/plan-enrichment.ts
|
|
14
|
+
import * as fs2 from "fs";
|
|
15
|
+
import * as path2 from "path";
|
|
16
|
+
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
17
|
+
|
|
18
|
+
// src/enrich-strategy.ts
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
function loadStrategy(repoRoot, name) {
|
|
22
|
+
const projectPath = path.join(repoRoot, ".glrs", "plan-enrich-strategies", `${name}.md`);
|
|
23
|
+
if (fs.existsSync(projectPath)) {
|
|
24
|
+
return fs.readFileSync(projectPath, "utf-8");
|
|
25
|
+
}
|
|
26
|
+
const builtinPath = path.join(__dirname, "strategies", `${name}.md`);
|
|
27
|
+
if (fs.existsSync(builtinPath)) {
|
|
28
|
+
return fs.readFileSync(builtinPath, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Unknown enrichment strategy "${name}". Searched:
|
|
31
|
+
${projectPath}
|
|
32
|
+
${builtinPath}`);
|
|
33
|
+
}
|
|
34
|
+
function extractFieldNames(strategy) {
|
|
35
|
+
const regex = /^\s*(?:-|\d+\.)\s+\*\*(\w+)\*\*:/gm;
|
|
36
|
+
const matches = [];
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = regex.exec(strategy)) !== null) {
|
|
39
|
+
matches.push(match[1]);
|
|
40
|
+
}
|
|
41
|
+
return matches.length > 0 ? matches : ["mirror", "context", "conventions"];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/plan-enrichment.ts
|
|
45
|
+
var ENRICHMENT_RATIO_THRESHOLD = 1;
|
|
46
|
+
function isFreeformFile(resolvedPath) {
|
|
47
|
+
try {
|
|
48
|
+
if (fs2.statSync(resolvedPath).isDirectory()) return false;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
let content;
|
|
53
|
+
try {
|
|
54
|
+
content = fs2.readFileSync(resolvedPath, "utf-8");
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const checkboxItems = (content.match(/^- \[[ xX]\]/gm) ?? []).length;
|
|
59
|
+
const headingItems = (content.match(/^###\s+\d+\.\d+\s/gm) ?? []).length;
|
|
60
|
+
return Math.max(checkboxItems, headingItems) === 0;
|
|
61
|
+
}
|
|
62
|
+
function buildDecompositionPrompt(cwd, planDir, sourceFile, content) {
|
|
63
|
+
return `You are decomposing a freeform document into a structured multi-file plan for automated execution.
|
|
64
|
+
|
|
65
|
+
Read the freeform content below and explore the codebase at ${cwd} to understand the project structure. Then write a structured plan as multiple files in the directory: ${planDir}
|
|
66
|
+
|
|
67
|
+
## Required output files
|
|
68
|
+
|
|
69
|
+
### 1. main.md
|
|
70
|
+
Write \`${planDir}/main.md\` with this structure:
|
|
71
|
+
|
|
72
|
+
\`\`\`markdown
|
|
73
|
+
# <Title derived from the document>
|
|
74
|
+
|
|
75
|
+
## Goal
|
|
76
|
+
<1-3 sentence summary of what this work accomplishes>
|
|
77
|
+
|
|
78
|
+
## Constraints
|
|
79
|
+
<Technical constraints, conventions to follow, or boundaries on the work>
|
|
80
|
+
|
|
81
|
+
## Phases
|
|
82
|
+
- [ ] wave_0.md - <phase description>
|
|
83
|
+
- [ ] wave_1.md - <phase description>
|
|
84
|
+
(add more phases as needed)
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
### 2. Phase files (wave_0.md, wave_1.md, etc.)
|
|
88
|
+
For each phase, write \`${planDir}/wave_N.md\` with numbered items:
|
|
89
|
+
|
|
90
|
+
\`\`\`markdown
|
|
91
|
+
# Wave N: <Phase title>
|
|
92
|
+
|
|
93
|
+
### N.1 <Item title>
|
|
94
|
+
- intent: <What this item accomplishes>
|
|
95
|
+
- files:
|
|
96
|
+
- <path/to/file.ts>
|
|
97
|
+
Change: <What changes in this file>
|
|
98
|
+
- tests:
|
|
99
|
+
- <path/to/test.ts>
|
|
100
|
+
- verify: <command to verify this item, e.g., "bun test path/to/test.ts">
|
|
101
|
+
|
|
102
|
+
### N.2 <Item title>
|
|
103
|
+
...
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
## Guidelines for decomposition
|
|
107
|
+
|
|
108
|
+
1. **Explore the codebase first.** Use file listing and reading tools to find:
|
|
109
|
+
- The project's test framework and test patterns
|
|
110
|
+
- Existing file naming conventions
|
|
111
|
+
- Related modules that items will modify or reference
|
|
112
|
+
- The package manager (bun/npm/pnpm/yarn) for verify commands
|
|
113
|
+
|
|
114
|
+
2. **Group items into phases by dependency.** Items in wave_0 should have no dependencies on later waves. Items within a wave should be independent or naturally sequential.
|
|
115
|
+
|
|
116
|
+
3. **Each item must be concrete and actionable.** Every item needs:
|
|
117
|
+
- A clear \`intent:\` describing the change
|
|
118
|
+
- Specific \`files:\` with real paths from the codebase
|
|
119
|
+
- At least one test file in \`tests:\`
|
|
120
|
+
- A runnable \`verify:\` command
|
|
121
|
+
|
|
122
|
+
4. **Keep items small.** Each item should be completable in a single focused session (1-3 files modified). Split large changes across multiple items.
|
|
123
|
+
|
|
124
|
+
5. **The heading format matters.** Use \`### N.M\` (e.g., \`### 0.1\`, \`### 0.2\`, \`### 1.1\`) \u2014 this is the format the enrichment pipeline recognizes.
|
|
125
|
+
|
|
126
|
+
## Source document
|
|
127
|
+
|
|
128
|
+
File: ${sourceFile}
|
|
129
|
+
|
|
130
|
+
\`\`\`markdown
|
|
131
|
+
${content}
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
Write all the plan files using the write/edit tool, then respond with "DECOMPOSITION_COMPLETE" when done.`;
|
|
135
|
+
}
|
|
136
|
+
async function decomposeFreeformPlan(cwd, resolvedPath, log, emitter, adapter, stallMs, config) {
|
|
137
|
+
const parsed = path2.parse(resolvedPath);
|
|
138
|
+
const planDir = path2.join(parsed.dir, parsed.name);
|
|
139
|
+
const mainMdPath = path2.join(planDir, "main.md");
|
|
140
|
+
if (fs2.existsSync(mainMdPath)) {
|
|
141
|
+
log?.info({ planDir }, "Decomposed plan directory already exists \u2014 skipping");
|
|
142
|
+
emitter?.emitEvent({
|
|
143
|
+
type: "enrich:file:skip",
|
|
144
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
145
|
+
file: path2.relative(cwd, resolvedPath),
|
|
146
|
+
reason: "decomposed directory already exists"
|
|
147
|
+
});
|
|
148
|
+
return planDir;
|
|
149
|
+
}
|
|
150
|
+
let content;
|
|
151
|
+
try {
|
|
152
|
+
content = fs2.readFileSync(resolvedPath, "utf-8");
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
log?.warn({ err: msg }, "Failed to read freeform file");
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
fs2.mkdirSync(planDir, { recursive: true });
|
|
159
|
+
emitter?.emitEvent({
|
|
160
|
+
type: "enrich:file:start",
|
|
161
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
162
|
+
file: path2.relative(cwd, resolvedPath)
|
|
163
|
+
});
|
|
164
|
+
const handle = await adapter.start({ cwd });
|
|
165
|
+
let sessionId;
|
|
166
|
+
try {
|
|
167
|
+
const adapterName = adapter.name;
|
|
168
|
+
const cfgObj = config;
|
|
169
|
+
const models = cfgObj?.models;
|
|
170
|
+
const enrichmentSpecifier = models?.enrichment;
|
|
171
|
+
const resolvedModel = enrichmentSpecifier ? resolveModel(enrichmentSpecifier, adapterName ?? "opencode") : void 0;
|
|
172
|
+
sessionId = await adapter.createSession(handle, {
|
|
173
|
+
agentName: "prime",
|
|
174
|
+
model: resolvedModel
|
|
175
|
+
});
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
178
|
+
log?.warn({ err: msg }, "createSession failed for decomposition");
|
|
179
|
+
await adapter.shutdown(handle);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const prompt = buildDecompositionPrompt(cwd, planDir, resolvedPath, content);
|
|
183
|
+
try {
|
|
184
|
+
const result = await adapter.sendAndWait(handle, {
|
|
185
|
+
sessionId,
|
|
186
|
+
message: prompt,
|
|
187
|
+
stallMs,
|
|
188
|
+
onToolCall: (toolName) => {
|
|
189
|
+
log?.debug({ toolName }, "Decomposition tool call");
|
|
190
|
+
},
|
|
191
|
+
onTextDelta: () => {
|
|
192
|
+
},
|
|
193
|
+
onCostUpdate: (cost, tokens) => {
|
|
194
|
+
emitter?.emitEvent({
|
|
195
|
+
type: "cost:update",
|
|
196
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
197
|
+
cumulativeCostUsd: cost,
|
|
198
|
+
isEstimated: false,
|
|
199
|
+
iteration: 0,
|
|
200
|
+
tokensIn: tokens.input,
|
|
201
|
+
tokensOut: tokens.output
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
if (result.kind === "error" || result.kind === "stall") {
|
|
206
|
+
const errMsg = result.kind === "error" ? "message" in result ? result.message : "session error" : "session stalled";
|
|
207
|
+
log?.error({ err: errMsg }, "Decomposition session failed");
|
|
208
|
+
emitter?.emitEvent({
|
|
209
|
+
type: "enrich:file:error",
|
|
210
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
211
|
+
file: path2.relative(cwd, resolvedPath),
|
|
212
|
+
error: `decomposition failed: ${errMsg}`
|
|
213
|
+
});
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const response = await adapter.getLastResponse(handle, sessionId);
|
|
217
|
+
if (!fs2.existsSync(mainMdPath)) {
|
|
218
|
+
log?.warn("Decomposition completed but main.md was not written");
|
|
219
|
+
emitter?.emitEvent({
|
|
220
|
+
type: "enrich:file:error",
|
|
221
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
|
+
file: path2.relative(cwd, resolvedPath),
|
|
223
|
+
error: "decomposition completed but main.md was not written"
|
|
224
|
+
});
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (!response.includes("DECOMPOSITION_COMPLETE")) {
|
|
228
|
+
log?.warn("Decomposition session did not emit completion sentinel");
|
|
229
|
+
}
|
|
230
|
+
log?.info({ planDir }, "Freeform file decomposed into structured plan");
|
|
231
|
+
emitter?.emitEvent({
|
|
232
|
+
type: "enrich:file:done",
|
|
233
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
234
|
+
file: path2.relative(cwd, resolvedPath),
|
|
235
|
+
toolCalls: 0
|
|
236
|
+
});
|
|
237
|
+
return planDir;
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
240
|
+
log?.error({ err: msg }, "Decomposition failed");
|
|
241
|
+
return null;
|
|
242
|
+
} finally {
|
|
243
|
+
await adapter.shutdown(handle);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function computeEnrichmentRatio(planFiles, fieldNames = ["mirror", "context", "conventions", "proof", "proof_type"]) {
|
|
247
|
+
let total = 0;
|
|
248
|
+
let enriched = 0;
|
|
249
|
+
for (const f of planFiles) {
|
|
250
|
+
let content;
|
|
251
|
+
try {
|
|
252
|
+
content = fs2.readFileSync(f, "utf-8");
|
|
253
|
+
} catch {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const checkboxItems = (content.match(/^- \[[ xX]\]/gm) ?? []).length;
|
|
257
|
+
const headingItems = (content.match(/^###\s+\d+\.\d+\s/gm) ?? []).length;
|
|
258
|
+
total += headingItems > 0 ? headingItems : checkboxItems;
|
|
259
|
+
const fieldCounts = fieldNames.map((field) => {
|
|
260
|
+
const regex = new RegExp(`^\\s*-?\\s*${field}:`, "gm");
|
|
261
|
+
return (content.match(regex) ?? []).length;
|
|
262
|
+
});
|
|
263
|
+
enriched += Math.min(...fieldCounts);
|
|
264
|
+
}
|
|
265
|
+
return total > 0 ? enriched / total : 0;
|
|
266
|
+
}
|
|
267
|
+
function computeSpecEnrichmentRatio(planDir, fieldNames = ["mirror", "context", "conventions", "proof", "proof_type"]) {
|
|
268
|
+
if (!hasSpec(planDir)) return 0;
|
|
269
|
+
const phaseFiles = detectSpecPhases(planDir);
|
|
270
|
+
let total = 0;
|
|
271
|
+
let enriched = 0;
|
|
272
|
+
for (const phaseFile of phaseFiles) {
|
|
273
|
+
const phasePath = path2.join(planDir, "spec", phaseFile);
|
|
274
|
+
let content;
|
|
275
|
+
try {
|
|
276
|
+
content = fs2.readFileSync(phasePath, "utf-8");
|
|
277
|
+
} catch {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const raw = yamlParse(content);
|
|
281
|
+
const items = Array.isArray(raw?.items) ? raw.items : [];
|
|
282
|
+
total += items.length;
|
|
283
|
+
for (const item of items) {
|
|
284
|
+
const hasAllFields = fieldNames.every((field) => item[field]);
|
|
285
|
+
if (hasAllFields) {
|
|
286
|
+
enriched++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return total > 0 ? enriched / total : 0;
|
|
291
|
+
}
|
|
292
|
+
function buildSpecGenerationPrompt(cwd, planDir, phaseFile, content, strategyName) {
|
|
293
|
+
const isMain = phaseFile === "main.md";
|
|
294
|
+
const specFileName = isMain ? "main.yaml" : phaseFile.replace(/\.md$/, ".yaml");
|
|
295
|
+
const specPath = `spec/${specFileName}`;
|
|
296
|
+
const schemaExample = isMain ? `\`\`\`yaml
|
|
297
|
+
# spec/main.yaml
|
|
298
|
+
title: "Plan title from H1"
|
|
299
|
+
goal: "Goal text"
|
|
300
|
+
constraints: "Constraints text"
|
|
301
|
+
phases:
|
|
302
|
+
- file: wave_0.yaml
|
|
303
|
+
completed: false
|
|
304
|
+
\`\`\`` : `\`\`\`yaml
|
|
305
|
+
# spec/${specFileName}
|
|
306
|
+
items:
|
|
307
|
+
- id: "0.1"
|
|
308
|
+
intent: "What this item does"
|
|
309
|
+
checked: false
|
|
310
|
+
files:
|
|
311
|
+
- path: src/foo.ts
|
|
312
|
+
isNew: false
|
|
313
|
+
change: "What changes"
|
|
314
|
+
tests:
|
|
315
|
+
- "test/foo.test.ts"
|
|
316
|
+
verify: "bun test test/foo.test.ts"
|
|
317
|
+
mirror: "src/similar-file.ts"
|
|
318
|
+
context: |
|
|
319
|
+
// relevant code from the file being modified
|
|
320
|
+
conventions: "ESM imports, named exports, bun:test"
|
|
321
|
+
proof: "The acceptance proof should verify that the new function accepts valid inputs and rejects invalid ones"
|
|
322
|
+
proof_type: "test"
|
|
323
|
+
\`\`\``;
|
|
324
|
+
const mainInstructions = `You are generating a YAML spec file from a markdown plan.
|
|
325
|
+
|
|
326
|
+
Read the markdown plan content below and write \`${specPath}\` (relative to the plan directory: ${planDir}) using the write/edit tool.
|
|
327
|
+
|
|
328
|
+
The output file should follow this schema:
|
|
329
|
+
|
|
330
|
+
${schemaExample}
|
|
331
|
+
|
|
332
|
+
Extract from the markdown:
|
|
333
|
+
- \`title\`: the H1 heading text
|
|
334
|
+
- \`goal\`: the Goal section text
|
|
335
|
+
- \`constraints\`: the Constraints section text (if present)
|
|
336
|
+
- \`phases\`: one entry per phase file referenced (e.g., wave_0.md \u2192 wave_0.yaml), all with \`completed: false\`
|
|
337
|
+
|
|
338
|
+
Here is the plan file to convert:
|
|
339
|
+
|
|
340
|
+
### ${phaseFile}
|
|
341
|
+
\`\`\`markdown
|
|
342
|
+
${content}
|
|
343
|
+
\`\`\`
|
|
344
|
+
|
|
345
|
+
Write the file \`${planDir}/${specPath}\` using the write/edit tool, then respond with "SPEC_COMPLETE" when done.`;
|
|
346
|
+
if (isMain) {
|
|
347
|
+
return mainInstructions;
|
|
348
|
+
}
|
|
349
|
+
const phaseTemplate = loadStrategy(cwd, strategyName ?? "default");
|
|
350
|
+
const phaseInstructions = phaseTemplate.replaceAll("{{specPath}}", specPath).replaceAll("{{planDir}}", planDir).replaceAll("{{specFileName}}", specFileName).replaceAll("{{phaseFile}}", phaseFile).replaceAll("{{content}}", content);
|
|
351
|
+
return phaseInstructions;
|
|
352
|
+
}
|
|
353
|
+
async function runEnrichmentPass(cwd, resolvedPath, planFiles, isDir, fieldNames, log, emitter, adapter, handle, stallMs, strategyName, config) {
|
|
354
|
+
let stallOccurred = false;
|
|
355
|
+
for (const f of planFiles) {
|
|
356
|
+
const rel = path2.relative(cwd, f);
|
|
357
|
+
const phaseFile = path2.basename(f);
|
|
358
|
+
const specFileName = phaseFile === "main.md" ? "main.yaml" : phaseFile.replace(/\.md$/, ".yaml");
|
|
359
|
+
const specPath = isDir ? path2.join(resolvedPath, "spec", specFileName) : null;
|
|
360
|
+
if (specPath && fs2.existsSync(specPath)) {
|
|
361
|
+
if (phaseFile === "main.md") {
|
|
362
|
+
log?.info({ file: rel }, "Spec already exists \u2014 skipping");
|
|
363
|
+
emitter?.emitEvent({
|
|
364
|
+
type: "enrich:file:skip",
|
|
365
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
366
|
+
file: rel,
|
|
367
|
+
reason: "spec already exists"
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const phaseItems = parseSpecItems(specPath);
|
|
372
|
+
if (phaseItems.some((it) => it.checked)) {
|
|
373
|
+
log?.info({ file: rel }, "Phase has checked items \u2014 skipping re-enrichment");
|
|
374
|
+
emitter?.emitEvent({
|
|
375
|
+
type: "enrich:file:skip",
|
|
376
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
377
|
+
file: rel,
|
|
378
|
+
reason: "phase has checked items (in-progress)"
|
|
379
|
+
});
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const phaseTotal = phaseItems.length;
|
|
383
|
+
const phaseEnriched = phaseItems.filter(
|
|
384
|
+
(it) => {
|
|
385
|
+
const item = it;
|
|
386
|
+
return fieldNames.every((field) => item[field]);
|
|
387
|
+
}
|
|
388
|
+
).length;
|
|
389
|
+
const phaseRatio = phaseTotal > 0 ? phaseEnriched / phaseTotal : 0;
|
|
390
|
+
if (phaseRatio >= ENRICHMENT_RATIO_THRESHOLD) {
|
|
391
|
+
log?.info({ file: rel, ratio: phaseRatio }, "Phase spec already enriched \u2014 skipping");
|
|
392
|
+
emitter?.emitEvent({
|
|
393
|
+
type: "enrich:file:skip",
|
|
394
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
395
|
+
file: rel,
|
|
396
|
+
reason: `already enriched (${Math.round(phaseRatio * 100)}%)`
|
|
397
|
+
});
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
let content;
|
|
402
|
+
try {
|
|
403
|
+
content = fs2.readFileSync(f, "utf-8");
|
|
404
|
+
} catch (err) {
|
|
405
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
406
|
+
log?.warn({ file: rel, err: msg }, "File read failed \u2014 skipping");
|
|
407
|
+
emitter?.emitEvent({
|
|
408
|
+
type: "enrich:file:error",
|
|
409
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
410
|
+
file: rel,
|
|
411
|
+
error: `read failed: ${msg}`
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (phaseFile !== "main.md") {
|
|
416
|
+
const checkboxItems = (content.match(/^- \[[ xX]\]/gm) ?? []).length;
|
|
417
|
+
const headingItems = (content.match(/^###\s+\d+\.\d+\s/gm) ?? []).length;
|
|
418
|
+
const itemCount = Math.max(checkboxItems, headingItems);
|
|
419
|
+
if (itemCount === 0) {
|
|
420
|
+
log?.info({ file: rel }, "No enrichable items \u2014 skipping");
|
|
421
|
+
emitter?.emitEvent({
|
|
422
|
+
type: "enrich:file:skip",
|
|
423
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
424
|
+
file: rel,
|
|
425
|
+
reason: "no enrichable items"
|
|
426
|
+
});
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
emitter?.emitEvent({
|
|
431
|
+
type: "enrich:file:start",
|
|
432
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
433
|
+
file: rel
|
|
434
|
+
});
|
|
435
|
+
let sessionId;
|
|
436
|
+
try {
|
|
437
|
+
const adapterName = adapter?.name;
|
|
438
|
+
const cfgObj = config;
|
|
439
|
+
const models = cfgObj?.models;
|
|
440
|
+
const enrichmentSpecifier = models?.enrichment;
|
|
441
|
+
const resolvedModel = enrichmentSpecifier ? resolveModel(enrichmentSpecifier, adapterName ?? "opencode") : void 0;
|
|
442
|
+
sessionId = await adapter.createSession(handle, {
|
|
443
|
+
agentName: "prime",
|
|
444
|
+
model: resolvedModel
|
|
445
|
+
});
|
|
446
|
+
} catch (err) {
|
|
447
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
448
|
+
log?.warn({ file: rel, err: msg }, "createSession failed \u2014 skipping");
|
|
449
|
+
emitter?.emitEvent({
|
|
450
|
+
type: "enrich:file:error",
|
|
451
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
452
|
+
file: rel,
|
|
453
|
+
error: `createSession failed: ${msg}`
|
|
454
|
+
});
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const prompt = buildSpecGenerationPrompt(cwd, resolvedPath, phaseFile, content, strategyName);
|
|
458
|
+
let toolCalls = 0;
|
|
459
|
+
let fileCost = 0;
|
|
460
|
+
try {
|
|
461
|
+
log?.info({ file: rel }, "Starting spec generation session");
|
|
462
|
+
const sessionResult = await adapter.sendAndWait(handle, {
|
|
463
|
+
sessionId,
|
|
464
|
+
message: prompt,
|
|
465
|
+
stallMs,
|
|
466
|
+
onToolCall: (toolName, firstArg) => {
|
|
467
|
+
toolCalls++;
|
|
468
|
+
log?.debug({ file: rel, toolName, firstArg, toolCalls }, "Spec generation tool call");
|
|
469
|
+
emitter?.emitEvent({
|
|
470
|
+
type: "tool:call",
|
|
471
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
472
|
+
toolName,
|
|
473
|
+
...firstArg ? { firstArg } : {},
|
|
474
|
+
iteration: 0
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
onTextDelta: () => {
|
|
478
|
+
},
|
|
479
|
+
onCostUpdate: (cost, tokens) => {
|
|
480
|
+
fileCost = cost;
|
|
481
|
+
emitter?.emitEvent({
|
|
482
|
+
type: "cost:update",
|
|
483
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
484
|
+
cumulativeCostUsd: cost,
|
|
485
|
+
isEstimated: false,
|
|
486
|
+
iteration: 0,
|
|
487
|
+
tokensIn: tokens.input,
|
|
488
|
+
tokensOut: tokens.output
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
if (sessionResult.kind === "error") {
|
|
493
|
+
const rawMsg = "message" in sessionResult ? sessionResult.message : "unknown error";
|
|
494
|
+
const errMsg = adapter.enhanceError ? await adapter.enhanceError(rawMsg) : rawMsg;
|
|
495
|
+
log?.error({ file: rel, err: errMsg }, "Session errored");
|
|
496
|
+
emitter?.emitEvent({
|
|
497
|
+
type: "enrich:file:error",
|
|
498
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
499
|
+
file: rel,
|
|
500
|
+
error: errMsg
|
|
501
|
+
});
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (sessionResult.kind === "stall") {
|
|
505
|
+
log?.error({ file: rel }, "Session stalled");
|
|
506
|
+
stallOccurred = true;
|
|
507
|
+
emitter?.emitEvent({
|
|
508
|
+
type: "enrich:file:error",
|
|
509
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
510
|
+
file: rel,
|
|
511
|
+
error: "session stalled"
|
|
512
|
+
});
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const response = await adapter.getLastResponse(handle, sessionId);
|
|
516
|
+
log?.debug({ file: rel, toolCalls, responseLength: response.length }, "Session response received");
|
|
517
|
+
if (adapter.getSessionStats) {
|
|
518
|
+
try {
|
|
519
|
+
const stats = await adapter.getSessionStats(handle, sessionId);
|
|
520
|
+
if (stats.tokensIn > 0 || stats.tokensOut > 0) {
|
|
521
|
+
emitter?.emitEvent({
|
|
522
|
+
type: "cost:update",
|
|
523
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
524
|
+
cumulativeCostUsd: stats.cost,
|
|
525
|
+
isEstimated: false,
|
|
526
|
+
iteration: 0,
|
|
527
|
+
tokensIn: stats.tokensIn,
|
|
528
|
+
tokensOut: stats.tokensOut
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (!response.includes("SPEC_COMPLETE")) {
|
|
535
|
+
log?.warn({ file: rel, toolCalls }, "Spec generation session did not complete cleanly");
|
|
536
|
+
emitter?.emitEvent({
|
|
537
|
+
type: "enrich:file:error",
|
|
538
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
539
|
+
file: rel,
|
|
540
|
+
error: "spec generation session did not complete cleanly"
|
|
541
|
+
});
|
|
542
|
+
} else if (specPath && !fs2.existsSync(specPath)) {
|
|
543
|
+
log?.warn({ file: rel, toolCalls }, "Spec generation session did not complete cleanly");
|
|
544
|
+
emitter?.emitEvent({
|
|
545
|
+
type: "enrich:file:error",
|
|
546
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
547
|
+
file: rel,
|
|
548
|
+
error: `SPEC_COMPLETE received but spec/${specFileName} was not written`
|
|
549
|
+
});
|
|
550
|
+
} else {
|
|
551
|
+
log?.info({ file: rel, toolCalls }, "Spec generated successfully");
|
|
552
|
+
emitter?.emitEvent({
|
|
553
|
+
type: "enrich:file:done",
|
|
554
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
555
|
+
file: rel,
|
|
556
|
+
toolCalls,
|
|
557
|
+
...specPath ? { specFile: `spec/${specFileName}` } : {}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
} catch (err) {
|
|
561
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
562
|
+
log?.error({ file: rel, err: msg, toolCalls }, "Spec generation failed \u2014 continuing");
|
|
563
|
+
emitter?.emitEvent({
|
|
564
|
+
type: "enrich:file:error",
|
|
565
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
566
|
+
file: rel,
|
|
567
|
+
error: `spec generation failed: ${msg}`
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return stallOccurred;
|
|
572
|
+
}
|
|
573
|
+
async function enrichPlanForFastModel(cwd, planPath, logger, emitter, adapter, enrichmentConfig, config) {
|
|
574
|
+
const log = logger ? childLogger(logger.root, "autopilot.enrichment") : void 0;
|
|
575
|
+
const resolvedPath = path2.resolve(cwd, planPath);
|
|
576
|
+
const isDir = fs2.existsSync(resolvedPath) && fs2.statSync(resolvedPath).isDirectory();
|
|
577
|
+
if (!isDir && isFreeformFile(resolvedPath)) {
|
|
578
|
+
if (!adapter) {
|
|
579
|
+
throw new Error("enrichPlanForFastModel: adapter is required for freeform decomposition");
|
|
580
|
+
}
|
|
581
|
+
log?.info({ file: resolvedPath }, "Freeform file detected \u2014 decomposing into structured plan");
|
|
582
|
+
const stallMs2 = enrichmentConfig?.stall_timeout ?? 5 * 60 * 1e3;
|
|
583
|
+
const decomposedDir = await decomposeFreeformPlan(
|
|
584
|
+
cwd,
|
|
585
|
+
resolvedPath,
|
|
586
|
+
log,
|
|
587
|
+
emitter,
|
|
588
|
+
adapter,
|
|
589
|
+
stallMs2,
|
|
590
|
+
config
|
|
591
|
+
);
|
|
592
|
+
if (decomposedDir) {
|
|
593
|
+
log?.info({ planDir: decomposedDir }, "Decomposition complete \u2014 enriching structured plan");
|
|
594
|
+
return enrichPlanForFastModel(
|
|
595
|
+
cwd,
|
|
596
|
+
decomposedDir,
|
|
597
|
+
logger,
|
|
598
|
+
emitter,
|
|
599
|
+
adapter,
|
|
600
|
+
enrichmentConfig,
|
|
601
|
+
config
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
log?.warn("Freeform decomposition failed \u2014 falling through to single-file enrichment");
|
|
605
|
+
}
|
|
606
|
+
let planFiles;
|
|
607
|
+
if (isDir) {
|
|
608
|
+
const entries = fs2.readdirSync(resolvedPath);
|
|
609
|
+
planFiles = entries.filter((f) => f.endsWith(".md") && f !== "scope.md" && f !== "scope-seed.md").sort((a, b) => {
|
|
610
|
+
if (a === "main.md") return -1;
|
|
611
|
+
if (b === "main.md") return 1;
|
|
612
|
+
const numA = parseInt(a.replace(/[^0-9]/g, ""), 10) || 0;
|
|
613
|
+
const numB = parseInt(b.replace(/[^0-9]/g, ""), 10) || 0;
|
|
614
|
+
return numA - numB;
|
|
615
|
+
}).map((f) => path2.join(resolvedPath, f));
|
|
616
|
+
} else {
|
|
617
|
+
planFiles = [resolvedPath];
|
|
618
|
+
}
|
|
619
|
+
emitter?.emitEvent({
|
|
620
|
+
type: "enrich:start",
|
|
621
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
622
|
+
planPath: resolvedPath,
|
|
623
|
+
fileCount: planFiles.length
|
|
624
|
+
});
|
|
625
|
+
let fieldNames = ["mirror", "context", "conventions", "proof", "proof_type"];
|
|
626
|
+
try {
|
|
627
|
+
const strategy = loadStrategy(cwd, "default");
|
|
628
|
+
fieldNames = extractFieldNames(strategy);
|
|
629
|
+
} catch (err) {
|
|
630
|
+
log?.warn({ err }, "Failed to load strategy \u2014 using defaults");
|
|
631
|
+
}
|
|
632
|
+
const isYamlSpec = isDir && hasSpec(resolvedPath);
|
|
633
|
+
const wholePlanRatio = isYamlSpec ? computeSpecEnrichmentRatio(resolvedPath, fieldNames) : computeEnrichmentRatio(planFiles, fieldNames);
|
|
634
|
+
if (wholePlanRatio >= ENRICHMENT_RATIO_THRESHOLD) {
|
|
635
|
+
log?.info({ ratio: wholePlanRatio }, "Plan already enriched \u2014 skipping");
|
|
636
|
+
emitter?.emitEvent({
|
|
637
|
+
type: "enrich:done",
|
|
638
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
639
|
+
filesProcessed: 0
|
|
640
|
+
});
|
|
641
|
+
return resolvedPath;
|
|
642
|
+
}
|
|
643
|
+
if (isDir) {
|
|
644
|
+
fs2.mkdirSync(path2.join(resolvedPath, "spec"), { recursive: true });
|
|
645
|
+
}
|
|
646
|
+
const existingMainYaml = path2.join(resolvedPath, "spec", "main.yaml");
|
|
647
|
+
let savedCompletionStates = null;
|
|
648
|
+
try {
|
|
649
|
+
if (fs2.existsSync(existingMainYaml)) {
|
|
650
|
+
const content = fs2.readFileSync(existingMainYaml, "utf-8");
|
|
651
|
+
const raw = yamlParse(content);
|
|
652
|
+
if (raw && Array.isArray(raw.phases)) {
|
|
653
|
+
savedCompletionStates = /* @__PURE__ */ new Map();
|
|
654
|
+
for (const phase of raw.phases) {
|
|
655
|
+
if (phase.file && phase.completed === true) {
|
|
656
|
+
savedCompletionStates.set(phase.file, true);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
const enableRetry = enrichmentConfig?.retry !== false;
|
|
664
|
+
const maxRetries = enrichmentConfig?.max_retries ?? 3;
|
|
665
|
+
const stallMs = enrichmentConfig?.stall_timeout ?? 5 * 60 * 1e3;
|
|
666
|
+
const strategyName = enrichmentConfig?.strategy;
|
|
667
|
+
if (!adapter) {
|
|
668
|
+
throw new Error("enrichPlanForFastModel: adapter is required");
|
|
669
|
+
}
|
|
670
|
+
log?.info({ planPath: resolvedPath, fileCount: planFiles.length, enableRetry, maxRetries }, "Starting enrichment");
|
|
671
|
+
let enrichmentCumulativeCost = 0;
|
|
672
|
+
if (!enableRetry) {
|
|
673
|
+
log?.info("Enrichment retry disabled \u2014 single attempt only");
|
|
674
|
+
const handle = await adapter.start({ cwd });
|
|
675
|
+
log?.info({ agentId: handle.id }, "Agent ready for enrichment");
|
|
676
|
+
try {
|
|
677
|
+
const stallOccurred = await runEnrichmentPass(
|
|
678
|
+
cwd,
|
|
679
|
+
resolvedPath,
|
|
680
|
+
planFiles,
|
|
681
|
+
isDir,
|
|
682
|
+
fieldNames,
|
|
683
|
+
log,
|
|
684
|
+
emitter,
|
|
685
|
+
adapter,
|
|
686
|
+
handle,
|
|
687
|
+
stallMs,
|
|
688
|
+
strategyName,
|
|
689
|
+
config
|
|
690
|
+
);
|
|
691
|
+
if (stallOccurred) {
|
|
692
|
+
log?.warn("Enrichment stalled but retry is disabled");
|
|
693
|
+
}
|
|
694
|
+
} finally {
|
|
695
|
+
await adapter.shutdown(handle);
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
let passSucceeded = false;
|
|
699
|
+
let lastError = null;
|
|
700
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
701
|
+
try {
|
|
702
|
+
log?.info({ attempt, maxRetries }, "Starting enrichment attempt");
|
|
703
|
+
const handle = await adapter.start({ cwd });
|
|
704
|
+
log?.info({ agentId: handle.id, attempt }, "Agent ready for enrichment");
|
|
705
|
+
try {
|
|
706
|
+
const stallOccurred = await runEnrichmentPass(
|
|
707
|
+
cwd,
|
|
708
|
+
resolvedPath,
|
|
709
|
+
planFiles,
|
|
710
|
+
isDir,
|
|
711
|
+
fieldNames,
|
|
712
|
+
log,
|
|
713
|
+
emitter,
|
|
714
|
+
adapter,
|
|
715
|
+
handle,
|
|
716
|
+
stallMs,
|
|
717
|
+
strategyName,
|
|
718
|
+
config
|
|
719
|
+
);
|
|
720
|
+
if (!stallOccurred) {
|
|
721
|
+
log?.info({ attempt }, "Enrichment pass completed without stalling");
|
|
722
|
+
passSucceeded = true;
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
log?.warn({ attempt }, "Enrichment pass stalled, will retry");
|
|
726
|
+
} finally {
|
|
727
|
+
await adapter.shutdown(handle);
|
|
728
|
+
}
|
|
729
|
+
} catch (err) {
|
|
730
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
731
|
+
const msg = lastError.message;
|
|
732
|
+
log?.warn({ attempt, err: msg }, "Enrichment attempt failed");
|
|
733
|
+
if (attempt === maxRetries) {
|
|
734
|
+
throw new Error(`Enrichment exhausted ${maxRetries} retries: ${msg}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (!passSucceeded) {
|
|
739
|
+
const msg = lastError?.message ?? "unknown error";
|
|
740
|
+
throw new Error(`Enrichment exhausted ${maxRetries} retries: ${msg}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (savedCompletionStates && savedCompletionStates.size > 0) {
|
|
744
|
+
try {
|
|
745
|
+
const mainYamlPath = path2.join(resolvedPath, "spec", "main.yaml");
|
|
746
|
+
if (fs2.existsSync(mainYamlPath)) {
|
|
747
|
+
let content = fs2.readFileSync(mainYamlPath, "utf-8");
|
|
748
|
+
const raw = yamlParse(content);
|
|
749
|
+
if (raw && Array.isArray(raw.phases)) {
|
|
750
|
+
let modified = false;
|
|
751
|
+
for (const phase of raw.phases) {
|
|
752
|
+
if (phase.file && savedCompletionStates.has(phase.file) && phase.completed !== true) {
|
|
753
|
+
phase.completed = true;
|
|
754
|
+
modified = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (modified) {
|
|
758
|
+
fs2.writeFileSync(mainYamlPath, yamlStringify(raw), "utf-8");
|
|
759
|
+
log?.info({ restored: savedCompletionStates.size }, "Restored phase completion states after enrichment");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch (err) {
|
|
764
|
+
log?.warn({ err }, "Failed to restore phase completion states \u2014 phases may need manual re-marking");
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
emitter?.emitEvent({
|
|
768
|
+
type: "enrich:done",
|
|
769
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
770
|
+
filesProcessed: planFiles.length
|
|
771
|
+
});
|
|
772
|
+
return resolvedPath;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export {
|
|
776
|
+
ENRICHMENT_RATIO_THRESHOLD,
|
|
777
|
+
isFreeformFile,
|
|
778
|
+
computeEnrichmentRatio,
|
|
779
|
+
computeSpecEnrichmentRatio,
|
|
780
|
+
enrichPlanForFastModel
|
|
781
|
+
};
|