@glrs-dev/cli 2.4.0 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/{chunk-HQUCVJ4G.js → chunk-FBXSGZAA.js} +4 -0
  3. package/dist/chunk-J3FXSHMA.js +263 -0
  4. package/dist/{chunk-5ZVUFNCP.js → chunk-S6N5E2GG.js} +8 -1
  5. package/dist/{chunk-2VMFXAJH.js → chunk-UO7WHIKY.js} +18 -5
  6. package/dist/cli.js +10 -3
  7. package/dist/commands/autopilot-tui.d.ts +11 -1
  8. package/dist/commands/autopilot-tui.js +2 -1
  9. package/dist/commands/autopilot.d.ts +2 -0
  10. package/dist/commands/autopilot.js +62 -21
  11. package/dist/commands/debrief.d.ts +2 -0
  12. package/dist/commands/debrief.js +1 -1
  13. package/dist/commands/loop.d.ts +2 -0
  14. package/dist/commands/loop.js +33 -12
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.d.ts +270 -0
  18. package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.js +506 -0
  19. package/dist/node_modules/@glrs-dev/adapter-opencode/package.json +8 -0
  20. package/dist/node_modules/@glrs-dev/autopilot/dist/auto-ship-EVLBKHUZ.js +7 -0
  21. package/dist/node_modules/@glrs-dev/autopilot/dist/changeset-generator-HAHYSSUR.js +15 -0
  22. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-2X3CWH47.js +3288 -0
  23. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-2ZQ6SBV3.js +70 -0
  24. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-6JZQLIRP.js +781 -0
  25. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-AWRK6S6G.js +91 -0
  26. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-BLEIZHET.js +101 -0
  27. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-GXXCEGDD.js +251 -0
  28. package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-S34HOCZ4.js +44 -0
  29. package/dist/node_modules/@glrs-dev/autopilot/dist/index.d.ts +1915 -0
  30. package/dist/node_modules/@glrs-dev/autopilot/dist/index.js +768 -0
  31. package/dist/node_modules/@glrs-dev/autopilot/dist/logger-3XLFMXLN.js +8 -0
  32. package/dist/node_modules/@glrs-dev/autopilot/dist/loop-session-YLCVJGPV.js +9 -0
  33. package/dist/node_modules/@glrs-dev/autopilot/dist/plan-enrichment-4SQYV5FC.js +17 -0
  34. package/dist/node_modules/@glrs-dev/autopilot/package.json +8 -0
  35. package/dist/vendor/harness-opencode/dist/agents/prompts/agents-md-writer.md +1 -1
  36. package/dist/vendor/harness-opencode/dist/agents/prompts/architecture-advisor.md +1 -1
  37. package/dist/vendor/harness-opencode/dist/agents/prompts/code-searcher.md +1 -1
  38. package/dist/vendor/harness-opencode/dist/agents/prompts/docs-maintainer.md +0 -8
  39. package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +1 -3
  40. package/dist/vendor/harness-opencode/dist/agents/prompts/lib-reader.md +1 -1
  41. package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +0 -2
  42. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +1 -1
  43. package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +78 -262
  44. package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +5 -14
  45. package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +7 -2
  46. package/dist/vendor/harness-opencode/dist/autopilot/strategies/default.md +29 -0
  47. package/dist/vendor/harness-opencode/dist/index.js +112 -82
  48. package/dist/vendor/harness-opencode/package.json +1 -1
  49. package/package.json +9 -7
@@ -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
+ };