@promptwheel/cli 0.6.0 → 0.7.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 (148) hide show
  1. package/dist/bin/promptwheel.js +1 -1
  2. package/dist/commands/solo-analytics.d.ts.map +1 -1
  3. package/dist/commands/solo-analytics.js +185 -2
  4. package/dist/commands/solo-analytics.js.map +1 -1
  5. package/dist/commands/solo-auto.d.ts.map +1 -1
  6. package/dist/commands/solo-auto.js +7 -6
  7. package/dist/commands/solo-auto.js.map +1 -1
  8. package/dist/commands/solo-daemon.d.ts.map +1 -1
  9. package/dist/commands/solo-daemon.js +4 -0
  10. package/dist/commands/solo-daemon.js.map +1 -1
  11. package/dist/commands/solo-inspect.d.ts.map +1 -1
  12. package/dist/commands/solo-inspect.js +93 -5
  13. package/dist/commands/solo-inspect.js.map +1 -1
  14. package/dist/commands/solo-nudge.d.ts.map +1 -1
  15. package/dist/commands/solo-nudge.js +38 -7
  16. package/dist/commands/solo-nudge.js.map +1 -1
  17. package/dist/commands/solo-trajectory.d.ts.map +1 -1
  18. package/dist/commands/solo-trajectory.js +23 -5
  19. package/dist/commands/solo-trajectory.js.map +1 -1
  20. package/dist/commands/solo.d.ts.map +1 -1
  21. package/dist/commands/solo.js +7 -1
  22. package/dist/commands/solo.js.map +1 -1
  23. package/dist/lib/cycle-context.d.ts +5 -0
  24. package/dist/lib/cycle-context.d.ts.map +1 -1
  25. package/dist/lib/cycle-context.js +12 -4
  26. package/dist/lib/cycle-context.js.map +1 -1
  27. package/dist/lib/daemon-fork.d.ts +1 -0
  28. package/dist/lib/daemon-fork.d.ts.map +1 -1
  29. package/dist/lib/daemon-fork.js +2 -0
  30. package/dist/lib/daemon-fork.js.map +1 -1
  31. package/dist/lib/daemon.d.ts +1 -0
  32. package/dist/lib/daemon.d.ts.map +1 -1
  33. package/dist/lib/daemon.js +2 -1
  34. package/dist/lib/daemon.js.map +1 -1
  35. package/dist/lib/display-adapter-log.d.ts +6 -0
  36. package/dist/lib/display-adapter-log.d.ts.map +1 -1
  37. package/dist/lib/display-adapter-log.js +3 -0
  38. package/dist/lib/display-adapter-log.js.map +1 -1
  39. package/dist/lib/display-adapter-spinner.d.ts +6 -0
  40. package/dist/lib/display-adapter-spinner.d.ts.map +1 -1
  41. package/dist/lib/display-adapter-spinner.js +4 -1
  42. package/dist/lib/display-adapter-spinner.js.map +1 -1
  43. package/dist/lib/display-adapter-tui.d.ts +6 -0
  44. package/dist/lib/display-adapter-tui.d.ts.map +1 -1
  45. package/dist/lib/display-adapter-tui.js +3 -0
  46. package/dist/lib/display-adapter-tui.js.map +1 -1
  47. package/dist/lib/display-adapter.d.ts +6 -0
  48. package/dist/lib/display-adapter.d.ts.map +1 -1
  49. package/dist/lib/error-ledger.d.ts +39 -0
  50. package/dist/lib/error-ledger.d.ts.map +1 -0
  51. package/dist/lib/error-ledger.js +73 -0
  52. package/dist/lib/error-ledger.js.map +1 -0
  53. package/dist/lib/goals.d.ts +1 -1
  54. package/dist/lib/goals.d.ts.map +1 -1
  55. package/dist/lib/goals.js +39 -7
  56. package/dist/lib/goals.js.map +1 -1
  57. package/dist/lib/learnings.d.ts.map +1 -1
  58. package/dist/lib/learnings.js +92 -68
  59. package/dist/lib/learnings.js.map +1 -1
  60. package/dist/lib/pr-outcomes.d.ts +42 -0
  61. package/dist/lib/pr-outcomes.d.ts.map +1 -0
  62. package/dist/lib/pr-outcomes.js +97 -0
  63. package/dist/lib/pr-outcomes.js.map +1 -0
  64. package/dist/lib/qa-stats.d.ts.map +1 -1
  65. package/dist/lib/qa-stats.js +3 -1
  66. package/dist/lib/qa-stats.js.map +1 -1
  67. package/dist/lib/retention.d.ts +3 -3
  68. package/dist/lib/retention.d.ts.map +1 -1
  69. package/dist/lib/retention.js +12 -12
  70. package/dist/lib/retention.js.map +1 -1
  71. package/dist/lib/run-history.d.ts +27 -0
  72. package/dist/lib/run-history.d.ts.map +1 -1
  73. package/dist/lib/run-history.js.map +1 -1
  74. package/dist/lib/run-state.d.ts +42 -0
  75. package/dist/lib/run-state.d.ts.map +1 -1
  76. package/dist/lib/run-state.js +66 -0
  77. package/dist/lib/run-state.js.map +1 -1
  78. package/dist/lib/session-report.d.ts.map +1 -1
  79. package/dist/lib/session-report.js +42 -0
  80. package/dist/lib/session-report.js.map +1 -1
  81. package/dist/lib/solo-auto-between-cycles.d.ts.map +1 -1
  82. package/dist/lib/solo-auto-between-cycles.js +381 -40
  83. package/dist/lib/solo-auto-between-cycles.js.map +1 -1
  84. package/dist/lib/solo-auto-drill.d.ts +228 -0
  85. package/dist/lib/solo-auto-drill.d.ts.map +1 -0
  86. package/dist/lib/solo-auto-drill.js +1229 -0
  87. package/dist/lib/solo-auto-drill.js.map +1 -0
  88. package/dist/lib/solo-auto-execute.d.ts +8 -0
  89. package/dist/lib/solo-auto-execute.d.ts.map +1 -1
  90. package/dist/lib/solo-auto-execute.js +99 -7
  91. package/dist/lib/solo-auto-execute.js.map +1 -1
  92. package/dist/lib/solo-auto-filter.js +3 -3
  93. package/dist/lib/solo-auto-filter.js.map +1 -1
  94. package/dist/lib/solo-auto-finalize.d.ts.map +1 -1
  95. package/dist/lib/solo-auto-finalize.js +32 -2
  96. package/dist/lib/solo-auto-finalize.js.map +1 -1
  97. package/dist/lib/solo-auto-init-qa.d.ts.map +1 -1
  98. package/dist/lib/solo-auto-init-qa.js +3 -1
  99. package/dist/lib/solo-auto-init-qa.js.map +1 -1
  100. package/dist/lib/solo-auto-scout.d.ts +1 -0
  101. package/dist/lib/solo-auto-scout.d.ts.map +1 -1
  102. package/dist/lib/solo-auto-scout.js +44 -13
  103. package/dist/lib/solo-auto-scout.js.map +1 -1
  104. package/dist/lib/solo-auto-state.d.ts.map +1 -1
  105. package/dist/lib/solo-auto-state.js +84 -24
  106. package/dist/lib/solo-auto-state.js.map +1 -1
  107. package/dist/lib/solo-auto-types.d.ts +64 -3
  108. package/dist/lib/solo-auto-types.d.ts.map +1 -1
  109. package/dist/lib/solo-auto.d.ts +3 -3
  110. package/dist/lib/solo-auto.d.ts.map +1 -1
  111. package/dist/lib/solo-auto.js +83 -4
  112. package/dist/lib/solo-auto.js.map +1 -1
  113. package/dist/lib/solo-config.d.ts +51 -3
  114. package/dist/lib/solo-config.d.ts.map +1 -1
  115. package/dist/lib/solo-config.js +43 -2
  116. package/dist/lib/solo-config.js.map +1 -1
  117. package/dist/lib/solo-hints.d.ts +10 -0
  118. package/dist/lib/solo-hints.d.ts.map +1 -1
  119. package/dist/lib/solo-hints.js +22 -0
  120. package/dist/lib/solo-hints.js.map +1 -1
  121. package/dist/lib/solo-session-summary.d.ts +12 -1
  122. package/dist/lib/solo-session-summary.d.ts.map +1 -1
  123. package/dist/lib/solo-session-summary.js +50 -6
  124. package/dist/lib/solo-session-summary.js.map +1 -1
  125. package/dist/lib/spindle-incidents.d.ts +29 -0
  126. package/dist/lib/spindle-incidents.d.ts.map +1 -0
  127. package/dist/lib/spindle-incidents.js +56 -0
  128. package/dist/lib/spindle-incidents.js.map +1 -0
  129. package/dist/lib/spinner.d.ts +1 -1
  130. package/dist/lib/taste-profile.d.ts.map +1 -1
  131. package/dist/lib/taste-profile.js +14 -8
  132. package/dist/lib/taste-profile.js.map +1 -1
  133. package/dist/lib/ticket-steps/step-spindle.d.ts.map +1 -1
  134. package/dist/lib/ticket-steps/step-spindle.js +14 -0
  135. package/dist/lib/ticket-steps/step-spindle.js.map +1 -1
  136. package/dist/lib/trajectory-generate.d.ts +73 -0
  137. package/dist/lib/trajectory-generate.d.ts.map +1 -1
  138. package/dist/lib/trajectory-generate.js +368 -12
  139. package/dist/lib/trajectory-generate.js.map +1 -1
  140. package/dist/lib/trajectory.d.ts +1 -1
  141. package/dist/lib/trajectory.d.ts.map +1 -1
  142. package/dist/lib/trajectory.js +67 -6
  143. package/dist/lib/trajectory.js.map +1 -1
  144. package/dist/tui/screens/auto.d.ts +7 -0
  145. package/dist/tui/screens/auto.d.ts.map +1 -1
  146. package/dist/tui/screens/auto.js +18 -1
  147. package/dist/tui/screens/auto.js.map +1 -1
  148. package/package.json +3 -3
@@ -5,7 +5,7 @@ import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
6
  import chalk from 'chalk';
7
7
  import { spawnSync } from 'node:child_process';
8
- import { readRunState, writeRunState, recordCycle, recordDocsAudit, getQualityRate } from './run-state.js';
8
+ import { readRunState, writeRunState, recordCycle, recordDocsAudit, getQualityRate, snapshotLearningROI } from './run-state.js';
9
9
  import { getSessionPhase } from './solo-auto-utils.js';
10
10
  import { checkPrStatuses, fetchPrReviewComments, deleteTicketBranch, deleteRemoteBranch, } from './solo-git.js';
11
11
  import { loadGuidelines } from './guidelines.js';
@@ -15,6 +15,7 @@ import { normalizeQaConfig } from './solo-utils.js';
15
15
  import { getPromptwheelDir } from './solo-config.js';
16
16
  import { removePrEntries } from './file-cooldown.js';
17
17
  import { recordFormulaMergeOutcome } from './run-state.js';
18
+ import { updatePrOutcome } from './pr-outcomes.js';
18
19
  import { recordMergeOutcome, saveSectors, refreshSectors, suggestScopeAdjustment, } from './sectors.js';
19
20
  import { loadDedupMemory } from './dedup-memory.js';
20
21
  import { calibrateConfidence } from './qa-stats.js';
@@ -25,7 +26,8 @@ import { buildTasteProfile, saveTasteProfile } from './taste-profile.js';
25
26
  import { runMeasurement, measureGoals, pickGoalByGap, recordGoalMeasurement, } from './goals.js';
26
27
  import { sleep } from './dedup.js';
27
28
  import { saveTrajectoryState } from './trajectory.js';
28
- import { getNextStep as getTrajectoryNextStep, trajectoryComplete, trajectoryStuck, } from '@promptwheel/core/trajectory/shared';
29
+ import { getNextStep as getTrajectoryNextStep, trajectoryComplete, trajectoryFullySucceeded, trajectoryStuck, } from '@promptwheel/core/trajectory/shared';
30
+ import { recordDrillTrajectoryOutcome, computeAmbitionLevel } from './solo-auto-drill.js';
29
31
  export async function runPreCycleMaintenance(state) {
30
32
  state.cycleCount++;
31
33
  state.cycleOutcomes = [];
@@ -72,7 +74,7 @@ export async function runPreCycleMaintenance(state) {
72
74
  }
73
75
  }
74
76
  // Backpressure from open PRs (skip in direct mode)
75
- if (state.runMode === 'wheel' && state.pendingPrUrls.length > 0 && state.deliveryMode !== 'direct') {
77
+ if (state.runMode === 'spin' && state.pendingPrUrls.length > 0 && state.deliveryMode !== 'direct') {
76
78
  const openRatio = state.pendingPrUrls.length / state.maxPrs;
77
79
  if (openRatio > 0.7) {
78
80
  console.log(chalk.yellow(` Backpressure: ${state.pendingPrUrls.length}/${state.maxPrs} PRs open — waiting for reviews...`));
@@ -109,7 +111,7 @@ export async function runPreCycleMaintenance(state) {
109
111
  }
110
112
  }
111
113
  // Periodic pull
112
- if (state.pullInterval > 0 && state.runMode === 'wheel') {
114
+ if (state.pullInterval > 0 && state.runMode === 'spin') {
113
115
  state.cyclesSinceLastPull++;
114
116
  if (state.cyclesSinceLastPull >= state.pullInterval) {
115
117
  state.cyclesSinceLastPull = 0;
@@ -132,11 +134,13 @@ export async function runPreCycleMaintenance(state) {
132
134
  console.log();
133
135
  console.log(chalk.bold('Resolution:'));
134
136
  console.log(` 1. Resolve the divergence (rebase, merge, or reset)`);
135
- console.log(` 2. Re-run: promptwheel --wheel`);
137
+ console.log(` 2. Re-run: promptwheel`);
136
138
  console.log();
137
139
  console.log(chalk.gray(` To keep going despite divergence, set pullPolicy: "warn" in config.`));
138
140
  // Signal orchestrator to break — finalizeSession handles cleanup
139
141
  state.shutdownRequested = true;
142
+ if (state.shutdownReason === null)
143
+ state.shutdownReason = 'branch_diverged';
140
144
  return { shouldSkipCycle: true };
141
145
  }
142
146
  else {
@@ -156,7 +160,7 @@ export async function runPreCycleMaintenance(state) {
156
160
  }
157
161
  }
158
162
  // Periodic PR status poll (every 5 cycles)
159
- if (state.runMode === 'wheel' && state.cycleCount > 1 && state.cycleCount % 5 === 0 && state.pendingPrUrls.length > 0) {
163
+ if (state.runMode === 'spin' && state.cycleCount > 1 && state.cycleCount % 5 === 0 && state.pendingPrUrls.length > 0) {
160
164
  try {
161
165
  const prStatuses = await checkPrStatuses(state.repoRoot, state.pendingPrUrls);
162
166
  for (const pr of prStatuses) {
@@ -168,6 +172,10 @@ export async function runPreCycleMaintenance(state) {
168
172
  recordMergeOutcome(state.sectorState, prMeta.sectorId, true);
169
173
  recordFormulaMergeOutcome(state.repoRoot, prMeta.formula, true);
170
174
  }
175
+ try {
176
+ updatePrOutcome(state.repoRoot, pr.url, 'merged', Date.now());
177
+ }
178
+ catch { /* non-fatal */ }
171
179
  if (state.autoConf.learningsEnabled) {
172
180
  addLearning(state.repoRoot, {
173
181
  text: `PR merged: ${pr.url}`.slice(0, 200),
@@ -190,6 +198,10 @@ export async function runPreCycleMaintenance(state) {
190
198
  recordMergeOutcome(state.sectorState, prMeta.sectorId, false);
191
199
  recordFormulaMergeOutcome(state.repoRoot, prMeta.formula, false);
192
200
  }
201
+ try {
202
+ updatePrOutcome(state.repoRoot, pr.url, 'closed', Date.now());
203
+ }
204
+ catch { /* non-fatal */ }
193
205
  if (state.autoConf.learningsEnabled) {
194
206
  addLearning(state.repoRoot, {
195
207
  text: `PR closed/rejected: ${pr.url}`.slice(0, 200),
@@ -312,17 +324,21 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
312
324
  if (recheckResult?.output)
313
325
  updatedDetails[name].output = recheckResult.output;
314
326
  }
315
- fs.writeFileSync(blPath, JSON.stringify({
327
+ const blTmp = blPath + '.tmp';
328
+ fs.writeFileSync(blTmp, JSON.stringify({
316
329
  failures: stillFailing,
317
330
  details: updatedDetails,
318
331
  timestamp: Date.now(),
319
332
  }));
333
+ fs.renameSync(blTmp, blPath);
320
334
  }
321
335
  }
322
336
  }
323
337
  }
324
338
  }
325
- catch { /* non-fatal */ }
339
+ catch (err) {
340
+ console.warn(chalk.gray(` Baseline healing skipped: ${err instanceof Error ? err.message : String(err)}`));
341
+ }
326
342
  }
327
343
  // Meta-learning extraction (aggregate pattern detection)
328
344
  let metaInsightsAdded = 0;
@@ -343,6 +359,24 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
343
359
  // Non-fatal
344
360
  }
345
361
  }
362
+ // Low-yield cycle detection — primary Nash equilibrium stop signal
363
+ const completedThisCount = state.cycleOutcomes.filter(o => o.status === 'completed').length;
364
+ if (completedThisCount === 0 && state.cycleCount >= 2) {
365
+ state.consecutiveLowYieldCycles++;
366
+ const MAX_LOW_YIELD_CYCLES = state.drillMode ? 5 : 3;
367
+ if (state.consecutiveLowYieldCycles >= MAX_LOW_YIELD_CYCLES) {
368
+ console.log(chalk.yellow(` ${state.consecutiveLowYieldCycles} consecutive low-yield cycles — diminishing returns, stopping`));
369
+ state.shutdownRequested = true;
370
+ if (state.shutdownReason === null)
371
+ state.shutdownReason = 'low_yield';
372
+ }
373
+ else if (state.options.verbose) {
374
+ console.log(chalk.gray(` Low-yield cycle (${state.consecutiveLowYieldCycles}/${MAX_LOW_YIELD_CYCLES})`));
375
+ }
376
+ }
377
+ else if (completedThisCount > 0) {
378
+ state.consecutiveLowYieldCycles = 0;
379
+ }
346
380
  // Wheel diagnostics one-liner (always shown, not verbose-gated)
347
381
  if (state.cycleCount >= 2) {
348
382
  const qualityRate = getQualityRate(state.repoRoot);
@@ -355,7 +389,7 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
355
389
  const confValue = state.effectiveMinConfidence;
356
390
  const insightsStr = metaInsightsAdded > 0 ? ` | insights +${metaInsightsAdded}` : '';
357
391
  const baselineStr = baselineFailing > 0 ? ` | baseline failing ${baselineFailing}` : '';
358
- console.log(chalk.gray(` Wheel: quality ${qualityPct}% | confidence ${confValue}${baselineStr}${insightsStr}`));
392
+ console.log(chalk.gray(` Spin: quality ${qualityPct}% | confidence ${confValue}${baselineStr}${insightsStr}`));
359
393
  }
360
394
  // Convergence metrics
361
395
  if (state.cycleCount >= 3 && state.sectorState) {
@@ -366,20 +400,85 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
366
400
  prsMerged: state.totalMergedPrs,
367
401
  prsClosed: state.totalClosedPrs,
368
402
  };
369
- const metrics = computeConvergenceMetrics(state.sectorState, state.allLearnings.length, rs.recentCycles ?? [], sessionCtx);
403
+ // Build drill context for convergence if in drill mode
404
+ let drillCtx;
405
+ if (state.drillMode && state.drillHistory.length >= 2) {
406
+ const { computeDrillMetrics } = await import('./solo-auto-drill.js');
407
+ const dm = computeDrillMetrics(state.drillHistory);
408
+ drillCtx = {
409
+ completionRate: dm.completionRate,
410
+ step1FailureRate: dm.step1FailureRate,
411
+ consecutiveInsufficient: state.drillConsecutiveInsufficient,
412
+ trajectoryCount: dm.totalTrajectories,
413
+ };
414
+ }
415
+ const metrics = computeConvergenceMetrics(state.sectorState, state.allLearnings.length, rs.recentCycles ?? [], sessionCtx, drillCtx);
370
416
  console.log(chalk.gray(` ${formatConvergenceOneLiner(metrics)}`));
371
417
  if (metrics.suggestedAction === 'stop') {
372
- console.log(chalk.yellow(` Convergence suggests stopping — most sectors polished, low yield.`));
373
- state.shutdownRequested = true;
418
+ if (state.activeTrajectory && state.activeTrajectoryState) {
419
+ // Adaptive threshold: use historical completion rate to decide when to abandon
420
+ // If we historically complete 80% of trajectories, a low-progress one is likely still worth finishing
421
+ // If we historically complete 20%, cut losses earlier
422
+ let abandonThreshold = 50; // default: stop if < 50% complete
423
+ if (state.drillMode && state.drillHistory.length >= 3) {
424
+ const { computeDrillMetrics: cdm } = await import('./solo-auto-drill.js');
425
+ const dm = cdm(state.drillHistory);
426
+ // Higher historical completion → higher threshold (more patience)
427
+ // Lower historical completion → lower threshold (cut losses faster)
428
+ abandonThreshold = Math.round(30 + (dm.weightedCompletionRate * 40)); // range: 30-70%
429
+ }
430
+ const totalSteps = state.activeTrajectory.steps.length;
431
+ const completedSteps = state.activeTrajectory.steps.filter(s => state.activeTrajectoryState.stepStates[s.id]?.status === 'completed').length;
432
+ const progressPct = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
433
+ if (progressPct < abandonThreshold) {
434
+ console.log(chalk.yellow(` Convergence suggests stopping — trajectory "${state.activeTrajectory.name}" only ${progressPct}% complete, skipping it`));
435
+ if (state.drillMode) {
436
+ try {
437
+ finishDrillTrajectory(state, 'stalled');
438
+ }
439
+ catch (err) {
440
+ console.log(chalk.yellow(` Drill: failed to record trajectory outcome — ${err instanceof Error ? err.message : String(err)}`));
441
+ }
442
+ }
443
+ state.activeTrajectory = null;
444
+ state.activeTrajectoryState = null;
445
+ state.currentTrajectoryStep = null;
446
+ state.shutdownRequested = true;
447
+ if (state.shutdownReason === null)
448
+ state.shutdownReason = 'convergence';
449
+ }
450
+ else {
451
+ console.log(chalk.gray(` Convergence suggests stopping, but trajectory "${state.activeTrajectory.name}" is ${progressPct}% complete — continuing`));
452
+ }
453
+ }
454
+ else {
455
+ console.log(chalk.yellow(` Convergence suggests stopping — most sectors polished, low yield.`));
456
+ state.shutdownRequested = true;
457
+ if (state.shutdownReason === null)
458
+ state.shutdownReason = 'convergence';
459
+ }
374
460
  }
375
461
  }
376
462
  // Scope adjustment (confidence only — impact uses static config floor)
377
463
  if (state.sectorState && state.cycleCount >= 3) {
378
464
  const scopeAdj = suggestScopeAdjustment(state.sectorState);
379
465
  if (scopeAdj === 'widen') {
380
- state.effectiveMinConfidence = state.autoConf.minConfidence ?? 20;
466
+ // In drill mode with active trajectory, don't widen — stay focused on trajectory scope
467
+ if (state.drillMode && state.currentTrajectoryStep?.scope) {
468
+ if (state.options.verbose)
469
+ console.log(chalk.gray(` Scope adjustment: drill mode — staying focused on trajectory scope`));
470
+ }
471
+ else {
472
+ state.effectiveMinConfidence = state.autoConf.minConfidence ?? 20;
473
+ if (state.options.verbose)
474
+ console.log(chalk.gray(` Scope adjustment: widening (resetting confidence threshold)`));
475
+ }
476
+ }
477
+ else if (scopeAdj === 'narrow' && state.drillMode && state.currentTrajectoryStep) {
478
+ // In drill mode, tighten confidence when trajectory-guided to focus on high-quality proposals
479
+ state.effectiveMinConfidence += 5;
381
480
  if (state.options.verbose)
382
- console.log(chalk.gray(` Scope adjustment: widening (resetting confidence threshold)`));
481
+ console.log(chalk.gray(` Scope adjustment: drill-narrowed (confidence +5)`));
383
482
  }
384
483
  }
385
484
  // Cross-sector pattern learning
@@ -401,6 +500,14 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
401
500
  }
402
501
  }
403
502
  }
503
+ // Learning ROI snapshot (every 10 cycles)
504
+ if (state.cycleCount % 10 === 0 && state.autoConf.learningsEnabled) {
505
+ try {
506
+ const { getLearningEffectiveness } = await import('./learnings.js');
507
+ snapshotLearningROI(state.repoRoot, getLearningEffectiveness);
508
+ }
509
+ catch { /* non-fatal */ }
510
+ }
404
511
  // Periodic learnings consolidation
405
512
  try {
406
513
  if (state.cycleCount % 5 === 0 && state.autoConf.learningsEnabled) {
@@ -415,8 +522,9 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
415
522
  }
416
523
  }
417
524
  }
418
- catch {
525
+ catch (err) {
419
526
  // Non-fatal — learnings persist from previous cycle
527
+ console.warn(chalk.gray(` Learnings consolidation skipped: ${err instanceof Error ? err.message : String(err)}`));
420
528
  }
421
529
  // Refresh codebase index
422
530
  if (state.codebaseIndex && hasStructuralChanges(state.codebaseIndex, state.repoRoot)) {
@@ -437,7 +545,7 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
437
545
  }
438
546
  }
439
547
  // Reload dedup memory
440
- if (state.runMode === 'wheel') {
548
+ if (state.runMode === 'spin') {
441
549
  state.dedupMemory = loadDedupMemory(state.repoRoot);
442
550
  }
443
551
  // Goal re-measurement
@@ -482,14 +590,28 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
482
590
  else {
483
591
  // Update current value for next cycle's prompt
484
592
  state.activeGoalMeasurement.current = value;
485
- // Recalculate gap
486
- if (direction === 'up' && target !== 0) {
487
- state.activeGoalMeasurement.gapPercent = Math.round(((target - value) / target) * 1000) / 10;
593
+ // Recalculate gap (guarded against division by zero)
594
+ if (direction === 'up') {
595
+ if (value >= target) {
596
+ state.activeGoalMeasurement.gapPercent = 0;
597
+ }
598
+ else if (target !== 0) {
599
+ state.activeGoalMeasurement.gapPercent = Math.round(((target - value) / target) * 1000) / 10;
600
+ }
601
+ else {
602
+ state.activeGoalMeasurement.gapPercent = 100;
603
+ }
488
604
  }
489
605
  else if (direction === 'down') {
490
- state.activeGoalMeasurement.gapPercent = target === 0
491
- ? (value > 0 ? 100 : 0)
492
- : Math.round(((value - target) / value) * 1000) / 10;
606
+ if (value <= target) {
607
+ state.activeGoalMeasurement.gapPercent = 0;
608
+ }
609
+ else if (value !== 0) {
610
+ state.activeGoalMeasurement.gapPercent = Math.round(((value - target) / value) * 1000) / 10;
611
+ }
612
+ else {
613
+ state.activeGoalMeasurement.gapPercent = 0;
614
+ }
493
615
  }
494
616
  }
495
617
  }
@@ -497,6 +619,30 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
497
619
  console.log(chalk.yellow(` ⚠ Goal "${state.activeGoal.name}" re-measurement failed${error ? `: ${error}` : ''}`));
498
620
  }
499
621
  }
622
+ // Trajectory cycle budget — abandon if consuming too many cycles.
623
+ // Scales with step count: more steps get more budget (2-step → base, 8-step → ~2x base).
624
+ if (state.drillMode && state.activeTrajectory && state.activeTrajectoryState) {
625
+ const baseMaxCycles = state.autoConf.drill?.maxCyclesPerTrajectory ?? 15;
626
+ const stepsTotal = state.activeTrajectory.steps.length;
627
+ const maxCycles = Math.round(baseMaxCycles * Math.min(2.5, Math.max(0.8, 1 + Math.max(0, stepsTotal - 3) / 5)));
628
+ const totalCyclesUsed = Object.values(state.activeTrajectoryState.stepStates)
629
+ .reduce((sum, s) => sum + (s.cyclesAttempted ?? 0), 0);
630
+ if (totalCyclesUsed >= maxCycles) {
631
+ const completedSteps = state.activeTrajectory.steps.filter(s => state.activeTrajectoryState.stepStates[s.id]?.status === 'completed').length;
632
+ const pct = Math.round((completedSteps / state.activeTrajectory.steps.length) * 100);
633
+ console.log(chalk.yellow(` Drill: trajectory "${state.activeTrajectory.name}" hit cycle budget (${totalCyclesUsed}/${maxCycles} cycles, ${pct}% complete) — abandoning`));
634
+ saveTrajectoryState(state.repoRoot, state.activeTrajectoryState);
635
+ try {
636
+ finishDrillTrajectory(state, 'stalled');
637
+ }
638
+ catch (err) {
639
+ console.log(chalk.yellow(` Drill: failed to record trajectory outcome — ${err instanceof Error ? err.message : String(err)}`));
640
+ }
641
+ state.activeTrajectory = null;
642
+ state.activeTrajectoryState = null;
643
+ state.currentTrajectoryStep = null;
644
+ }
645
+ }
500
646
  // Trajectory step progression
501
647
  if (state.activeTrajectory && state.activeTrajectoryState && state.currentTrajectoryStep) {
502
648
  const step = state.currentTrajectoryStep;
@@ -504,40 +650,108 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
504
650
  if (stepState) {
505
651
  // Run step verification commands
506
652
  let allPassed = true;
653
+ const verificationOutputParts = [];
507
654
  if (step.verification_commands.length > 0) {
508
- allPassed = step.verification_commands.every(cmd => {
509
- const result = spawnSync('sh', ['-c', cmd], { cwd: state.repoRoot, timeout: 30000 });
510
- return result.status === 0;
511
- });
655
+ for (const cmd of step.verification_commands) {
656
+ const result = spawnSync('sh', ['-c', cmd], {
657
+ cwd: state.repoRoot,
658
+ timeout: 30000,
659
+ encoding: 'utf-8',
660
+ });
661
+ if (result.error) {
662
+ // Timeout or spawn error
663
+ allPassed = false;
664
+ const reason = result.error.message?.includes('TIMEOUT') ? 'timeout (30s)' : result.error.message;
665
+ console.log(chalk.yellow(` ✗ ${cmd} (${reason})`));
666
+ verificationOutputParts.push(`$ ${cmd}\n${reason}`);
667
+ }
668
+ else if (result.status !== 0) {
669
+ allPassed = false;
670
+ const stderr = (result.stderr || '').trim().slice(0, 500);
671
+ const stdout = (result.stdout || '').trim().slice(0, 200);
672
+ console.log(chalk.yellow(` ✗ ${cmd} (exit ${result.status})`));
673
+ if (stderr)
674
+ console.log(chalk.gray(` ${stderr.split('\n')[0]}`));
675
+ else if (stdout)
676
+ console.log(chalk.gray(` ${stdout.split('\n')[0]}`));
677
+ verificationOutputParts.push(`$ ${cmd} (exit ${result.status})\n${stderr || stdout}`);
678
+ }
679
+ }
512
680
  }
513
681
  // Optional measurement check
514
682
  let measureMet = true;
515
683
  if (step.measure) {
516
- const { value } = runMeasurement(step.measure.cmd, state.repoRoot);
684
+ const { value, error } = runMeasurement(step.measure.cmd, state.repoRoot);
517
685
  if (value !== null) {
686
+ const arrow = step.measure.direction === 'up' ? '>=' : '<=';
518
687
  measureMet = step.measure.direction === 'up'
519
688
  ? value >= step.measure.target
520
689
  : value <= step.measure.target;
521
690
  stepState.measurement = { value, timestamp: Date.now() };
691
+ if (!measureMet) {
692
+ console.log(chalk.yellow(` measure: ${value} (target: ${arrow} ${step.measure.target})`));
693
+ }
694
+ }
695
+ else {
696
+ measureMet = false;
697
+ console.log(chalk.yellow(` measure failed${error ? `: ${error}` : ''}`));
522
698
  }
523
699
  }
524
- if (allPassed && measureMet && step.verification_commands.length > 0) {
700
+ if (allPassed && measureMet) {
525
701
  // Step completed — advance
526
702
  stepState.status = 'completed';
527
703
  stepState.completedAt = Date.now();
528
- console.log(chalk.green(` Trajectory step "${step.title}" completed`));
704
+ stepState.consecutiveFailures = 0;
705
+ stepState.lastVerificationOutput = undefined;
706
+ const completedCount = state.activeTrajectory.steps.filter(s => state.activeTrajectoryState.stepStates[s.id]?.status === 'completed').length;
707
+ const totalCount = state.activeTrajectory.steps.length;
708
+ console.log(chalk.green(` Trajectory step ${completedCount}/${totalCount} "${step.title}" completed`));
529
709
  // Pick next step
530
710
  const next = getTrajectoryNextStep(state.activeTrajectory, state.activeTrajectoryState.stepStates);
531
711
  state.currentTrajectoryStep = next;
532
712
  if (next) {
533
713
  state.activeTrajectoryState.currentStepId = next.id;
534
- state.activeTrajectoryState.stepStates[next.id].status = 'active';
714
+ if (state.activeTrajectoryState.stepStates[next.id]) {
715
+ state.activeTrajectoryState.stepStates[next.id].status = 'active';
716
+ }
535
717
  console.log(chalk.cyan(` -> Next step: ${next.title}`));
536
718
  }
537
719
  else if (trajectoryComplete(state.activeTrajectory, state.activeTrajectoryState.stepStates)) {
538
- console.log(chalk.green(` Trajectory "${state.activeTrajectory.name}" complete!`));
720
+ const fullySucceeded = trajectoryFullySucceeded(state.activeTrajectory, state.activeTrajectoryState.stepStates);
721
+ const outcome = fullySucceeded ? 'completed' : 'stalled';
722
+ if (fullySucceeded) {
723
+ console.log(chalk.green(` Trajectory "${state.activeTrajectory.name}" complete!`));
724
+ }
725
+ else {
726
+ console.log(chalk.yellow(` Trajectory "${state.activeTrajectory.name}" finished with some failed steps`));
727
+ }
539
728
  // Save final state before clearing (so completed status persists on disk)
540
729
  saveTrajectoryState(state.repoRoot, state.activeTrajectoryState);
730
+ if (state.drillMode) {
731
+ try {
732
+ finishDrillTrajectory(state, outcome);
733
+ }
734
+ catch (err) {
735
+ console.log(chalk.yellow(` Drill: failed to record trajectory outcome — ${err instanceof Error ? err.message : String(err)}`));
736
+ }
737
+ }
738
+ state.activeTrajectory = null;
739
+ state.activeTrajectoryState = null;
740
+ state.currentTrajectoryStep = null;
741
+ }
742
+ else {
743
+ // No next step available but trajectory isn't complete — shouldn't happen now
744
+ // (failed deps unblock dependents), but handle as fallback
745
+ console.log(chalk.yellow(` Trajectory "${state.activeTrajectory.name}" stalled (remaining steps blocked)`));
746
+ saveTrajectoryState(state.repoRoot, state.activeTrajectoryState);
747
+ if (state.drillMode) {
748
+ try {
749
+ finishDrillTrajectory(state, 'stalled');
750
+ }
751
+ catch (err) {
752
+ console.log(chalk.yellow(` Drill: failed to record trajectory outcome — ${err instanceof Error ? err.message : String(err)}`));
753
+ }
754
+ }
541
755
  state.activeTrajectory = null;
542
756
  state.activeTrajectoryState = null;
543
757
  state.currentTrajectoryStep = null;
@@ -547,20 +761,52 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
547
761
  // Step not yet complete — increment attempt counter
548
762
  stepState.cyclesAttempted++;
549
763
  stepState.lastAttemptedCycle = state.cycleCount;
550
- // Check for stuck
551
- const stuckId = trajectoryStuck(state.activeTrajectoryState.stepStates);
764
+ // Track consecutive and total failures for transient/flakiness detection
765
+ stepState.consecutiveFailures = (stepState.consecutiveFailures ?? 0) + 1;
766
+ stepState.totalFailures = (stepState.totalFailures ?? 0) + 1;
767
+ // Capture verification output for prompt injection on next attempt
768
+ if (verificationOutputParts.length > 0) {
769
+ stepState.lastVerificationOutput = verificationOutputParts.join('\n').slice(0, 1000);
770
+ }
771
+ // Check for stuck — pass full step list so each step uses its own max_retries
772
+ const stuckId = trajectoryStuck(state.activeTrajectoryState.stepStates, undefined, state.activeTrajectory.steps);
552
773
  if (stuckId) {
553
- console.log(chalk.yellow(` Trajectory step "${step.title}" stuck after ${stepState.cyclesAttempted} cycles`));
554
- stepState.status = 'failed';
555
- stepState.failureReason = 'max retries exceeded';
774
+ // Fail the actual stuck step (may differ from current step if state was corrupted)
775
+ const stuckStepState = state.activeTrajectoryState.stepStates[stuckId];
776
+ const stuckStep = state.activeTrajectory.steps.find(s => s.id === stuckId);
777
+ const stuckTitle = stuckStep?.title ?? stuckId;
778
+ const stuckAttempts = stuckStepState?.cyclesAttempted ?? stepState.cyclesAttempted;
779
+ console.log(chalk.yellow(` Trajectory step "${stuckTitle}" stuck after ${stuckAttempts} cycles`));
780
+ if (stuckStepState) {
781
+ stuckStepState.status = 'failed';
782
+ stuckStepState.failureReason = 'max retries exceeded';
783
+ }
556
784
  // Try to advance to next step
557
785
  const next = getTrajectoryNextStep(state.activeTrajectory, state.activeTrajectoryState.stepStates);
558
786
  state.currentTrajectoryStep = next;
559
787
  if (next) {
560
788
  state.activeTrajectoryState.currentStepId = next.id;
561
- state.activeTrajectoryState.stepStates[next.id].status = 'active';
789
+ if (state.activeTrajectoryState.stepStates[next.id]) {
790
+ state.activeTrajectoryState.stepStates[next.id].status = 'active';
791
+ }
562
792
  console.log(chalk.cyan(` -> Skipping to next step: ${next.title}`));
563
793
  }
794
+ else {
795
+ // No more steps — trajectory is done (all remaining steps failed or completed)
796
+ console.log(chalk.yellow(` Trajectory "${state.activeTrajectory.name}" ended (no remaining steps)`));
797
+ saveTrajectoryState(state.repoRoot, state.activeTrajectoryState);
798
+ if (state.drillMode) {
799
+ try {
800
+ finishDrillTrajectory(state, 'stalled');
801
+ }
802
+ catch (err) {
803
+ console.log(chalk.yellow(` Drill: failed to record trajectory outcome — ${err instanceof Error ? err.message : String(err)}`));
804
+ }
805
+ }
806
+ state.activeTrajectory = null;
807
+ state.activeTrajectoryState = null;
808
+ state.currentTrajectoryStep = null;
809
+ }
564
810
  }
565
811
  }
566
812
  if (state.activeTrajectoryState) {
@@ -568,10 +814,105 @@ export async function runPostCycleMaintenance(state, scope, isDocsAuditCycle) {
568
814
  }
569
815
  }
570
816
  }
571
- // Pause between cycles
572
- if (state.runMode === 'wheel' && !state.shutdownRequested) {
817
+ // Pause between cycles — shorter when trajectory-guided (work is pre-planned)
818
+ if (state.runMode === 'spin' && !state.shutdownRequested) {
819
+ const pauseMs = state.currentTrajectoryStep ? 1000 : 5000;
573
820
  console.log(chalk.gray('Pausing before next cycle...'));
574
- await sleep(5000);
821
+ await sleep(pauseMs);
822
+ }
823
+ }
824
+ // ── Drill trajectory lifecycle ───────────────────────────────────────────────
825
+ /**
826
+ * Record a drill trajectory's completion/stall into history, record learnings,
827
+ * and log the next-survey message.
828
+ *
829
+ * Must be called BEFORE clearing state.activeTrajectory (needs the trajectory data).
830
+ */
831
+ function finishDrillTrajectory(state, outcome) {
832
+ if (!state.activeTrajectory || !state.activeTrajectoryState)
833
+ return;
834
+ const traj = state.activeTrajectory;
835
+ const trajState = state.activeTrajectoryState;
836
+ const stepsTotal = traj.steps.length;
837
+ const stepsCompleted = traj.steps.filter(s => trajState.stepStates[s.id]?.status === 'completed').length;
838
+ const stepsFailed = traj.steps.filter(s => trajState.stepStates[s.id]?.status === 'failed').length;
839
+ // Collect failed step details for history
840
+ const failedStepDetails = traj.steps
841
+ .filter(s => trajState.stepStates[s.id]?.status === 'failed')
842
+ .map(s => ({
843
+ id: s.id,
844
+ title: s.title,
845
+ reason: trajState.stepStates[s.id]?.lastVerificationOutput?.slice(0, 200)
846
+ ?? trajState.stepStates[s.id]?.failureReason,
847
+ }));
848
+ // Collect completed step summaries for causal chaining
849
+ const completedStepSummaries = traj.steps
850
+ .filter(s => trajState.stepStates[s.id]?.status === 'completed')
851
+ .map(s => s.title);
852
+ // Collect modified files from git (since trajectory started)
853
+ let modifiedFiles;
854
+ try {
855
+ const trajStartTime = trajState.startedAt ?? (state.startTime || 0);
856
+ if (trajStartTime > 0) {
857
+ // Use git log --diff-filter with --since instead of HEAD~N (which fails with shallow repos or few commits)
858
+ const sinceDate = new Date(trajStartTime).toISOString();
859
+ const gitResult = spawnSync('git', [
860
+ 'log', '--diff-filter=ACMR', '--name-only', '--pretty=format:',
861
+ `--since=${sinceDate}`,
862
+ ], { cwd: state.repoRoot, encoding: 'utf-8', timeout: 5000 });
863
+ if (!gitResult.error && gitResult.status === 0 && gitResult.stdout.trim()) {
864
+ // Deduplicate file names (same file may appear in multiple commits)
865
+ modifiedFiles = [...new Set(gitResult.stdout.trim().split('\n').filter(Boolean))].slice(0, 20);
866
+ }
867
+ }
868
+ }
869
+ catch { /* non-fatal */ }
870
+ // Collect per-step outcomes for telemetry (enables step-level learning)
871
+ const stepOutcomes = traj.steps.map(s => ({
872
+ id: s.id,
873
+ status: (trajState.stepStates[s.id]?.status ?? 'pending'),
874
+ }));
875
+ // Record into drill history (for avoidance + diversity + stats)
876
+ recordDrillTrajectoryOutcome(state, traj.name, traj.description, stepsTotal, stepsCompleted, stepsFailed, outcome, traj.steps, failedStepDetails.length > 0 ? failedStepDetails : undefined, completedStepSummaries.length > 0 ? completedStepSummaries : undefined, modifiedFiles, computeAmbitionLevel(state), {
877
+ stepOutcomes,
878
+ ...state.drillGenerationTelemetry,
879
+ });
880
+ state.drillGenerationTelemetry = null;
881
+ // Record learnings
882
+ if (state.autoConf.learningsEnabled) {
883
+ const categories = [...new Set(traj.steps.flatMap(s => s.categories ?? []))];
884
+ const catLabel = categories.join(', ') || 'mixed';
885
+ if (outcome === 'completed') {
886
+ addLearning(state.repoRoot, {
887
+ text: `Drill trajectory "${traj.name}" completed (${stepsCompleted}/${stepsTotal} steps). Theme: ${traj.description}. Categories: ${catLabel}`.slice(0, 200),
888
+ category: 'pattern',
889
+ source: { type: 'drill_completed', detail: traj.name },
890
+ tags: categories,
891
+ });
892
+ }
893
+ else {
894
+ const failedSteps = traj.steps
895
+ .filter(s => trajState.stepStates[s.id]?.status === 'failed')
896
+ .map(s => s.title);
897
+ addLearning(state.repoRoot, {
898
+ text: `Drill trajectory "${traj.name}" stalled (${stepsCompleted}/${stepsTotal} completed, ${stepsFailed} failed). Failed: ${failedSteps.join(', ')}`.slice(0, 200),
899
+ category: 'warning',
900
+ source: { type: 'drill_stalled', detail: traj.name },
901
+ tags: categories,
902
+ });
903
+ }
904
+ }
905
+ const rate = stepsTotal > 0 ? Math.round((stepsCompleted / stepsTotal) * 100) : 0;
906
+ console.log(chalk.cyan(` Drill: trajectory ${outcome} (${stepsCompleted}/${stepsTotal} steps, ${rate}% completion)`));
907
+ console.log(chalk.cyan(' Drill: will survey for next trajectory on next cycle'));
908
+ // Notify display adapter that trajectory finished (back to idle)
909
+ state.displayAdapter.drillStateChanged({ active: true });
910
+ // Reload learnings immediately so next trajectory generation has fresh context
911
+ if (state.autoConf.learningsEnabled) {
912
+ try {
913
+ state.allLearnings = loadLearnings(state.repoRoot, 0);
914
+ }
915
+ catch { /* non-fatal */ }
575
916
  }
576
917
  }
577
918
  //# sourceMappingURL=solo-auto-between-cycles.js.map