@promptwheel/cli 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/promptwheel.d.ts +1 -1
- package/dist/bin/promptwheel.js +2 -2
- package/dist/commands/auto-auth.js +2 -2
- package/dist/commands/auto-auth.js.map +1 -1
- package/dist/commands/solo-analytics.d.ts.map +1 -1
- package/dist/commands/solo-analytics.js +185 -2
- package/dist/commands/solo-analytics.js.map +1 -1
- package/dist/commands/solo-auto.d.ts.map +1 -1
- package/dist/commands/solo-auto.js +11 -12
- package/dist/commands/solo-auto.js.map +1 -1
- package/dist/commands/solo-daemon.d.ts.map +1 -1
- package/dist/commands/solo-daemon.js +4 -0
- package/dist/commands/solo-daemon.js.map +1 -1
- package/dist/commands/solo-inspect.d.ts.map +1 -1
- package/dist/commands/solo-inspect.js +93 -5
- package/dist/commands/solo-inspect.js.map +1 -1
- package/dist/commands/solo-nudge.d.ts.map +1 -1
- package/dist/commands/solo-nudge.js +38 -7
- package/dist/commands/solo-nudge.js.map +1 -1
- package/dist/commands/solo-trajectory.d.ts.map +1 -1
- package/dist/commands/solo-trajectory.js +23 -5
- package/dist/commands/solo-trajectory.js.map +1 -1
- package/dist/commands/solo.d.ts.map +1 -1
- package/dist/commands/solo.js +8 -2
- package/dist/commands/solo.js.map +1 -1
- package/dist/lib/cycle-context.d.ts +5 -0
- package/dist/lib/cycle-context.d.ts.map +1 -1
- package/dist/lib/cycle-context.js +12 -4
- package/dist/lib/cycle-context.js.map +1 -1
- package/dist/lib/daemon-fork.d.ts +1 -0
- package/dist/lib/daemon-fork.d.ts.map +1 -1
- package/dist/lib/daemon-fork.js +2 -0
- package/dist/lib/daemon-fork.js.map +1 -1
- package/dist/lib/daemon.d.ts +1 -0
- package/dist/lib/daemon.d.ts.map +1 -1
- package/dist/lib/daemon.js +2 -1
- package/dist/lib/daemon.js.map +1 -1
- package/dist/lib/display-adapter-log.d.ts +6 -0
- package/dist/lib/display-adapter-log.d.ts.map +1 -1
- package/dist/lib/display-adapter-log.js +3 -0
- package/dist/lib/display-adapter-log.js.map +1 -1
- package/dist/lib/display-adapter-spinner.d.ts +6 -0
- package/dist/lib/display-adapter-spinner.d.ts.map +1 -1
- package/dist/lib/display-adapter-spinner.js +4 -1
- package/dist/lib/display-adapter-spinner.js.map +1 -1
- package/dist/lib/display-adapter-tui.d.ts +6 -0
- package/dist/lib/display-adapter-tui.d.ts.map +1 -1
- package/dist/lib/display-adapter-tui.js +3 -0
- package/dist/lib/display-adapter-tui.js.map +1 -1
- package/dist/lib/display-adapter.d.ts +6 -0
- package/dist/lib/display-adapter.d.ts.map +1 -1
- package/dist/lib/error-ledger.d.ts +39 -0
- package/dist/lib/error-ledger.d.ts.map +1 -0
- package/dist/lib/error-ledger.js +73 -0
- package/dist/lib/error-ledger.js.map +1 -0
- package/dist/lib/goals.d.ts +1 -1
- package/dist/lib/goals.d.ts.map +1 -1
- package/dist/lib/goals.js +39 -7
- package/dist/lib/goals.js.map +1 -1
- package/dist/lib/learnings.d.ts.map +1 -1
- package/dist/lib/learnings.js +92 -68
- package/dist/lib/learnings.js.map +1 -1
- package/dist/lib/pr-outcomes.d.ts +42 -0
- package/dist/lib/pr-outcomes.d.ts.map +1 -0
- package/dist/lib/pr-outcomes.js +97 -0
- package/dist/lib/pr-outcomes.js.map +1 -0
- package/dist/lib/qa-stats.d.ts.map +1 -1
- package/dist/lib/qa-stats.js +3 -1
- package/dist/lib/qa-stats.js.map +1 -1
- package/dist/lib/retention.d.ts +3 -3
- package/dist/lib/retention.d.ts.map +1 -1
- package/dist/lib/retention.js +12 -12
- package/dist/lib/retention.js.map +1 -1
- package/dist/lib/run-history.d.ts +27 -0
- package/dist/lib/run-history.d.ts.map +1 -1
- package/dist/lib/run-history.js.map +1 -1
- package/dist/lib/run-state.d.ts +42 -0
- package/dist/lib/run-state.d.ts.map +1 -1
- package/dist/lib/run-state.js +66 -0
- package/dist/lib/run-state.js.map +1 -1
- package/dist/lib/session-report.d.ts.map +1 -1
- package/dist/lib/session-report.js +42 -0
- package/dist/lib/session-report.js.map +1 -1
- package/dist/lib/solo-auto-between-cycles.d.ts.map +1 -1
- package/dist/lib/solo-auto-between-cycles.js +381 -40
- package/dist/lib/solo-auto-between-cycles.js.map +1 -1
- package/dist/lib/solo-auto-drill.d.ts +228 -0
- package/dist/lib/solo-auto-drill.d.ts.map +1 -0
- package/dist/lib/solo-auto-drill.js +1229 -0
- package/dist/lib/solo-auto-drill.js.map +1 -0
- package/dist/lib/solo-auto-execute.d.ts +8 -0
- package/dist/lib/solo-auto-execute.d.ts.map +1 -1
- package/dist/lib/solo-auto-execute.js +99 -7
- package/dist/lib/solo-auto-execute.js.map +1 -1
- package/dist/lib/solo-auto-filter.js +3 -3
- package/dist/lib/solo-auto-filter.js.map +1 -1
- package/dist/lib/solo-auto-finalize.d.ts.map +1 -1
- package/dist/lib/solo-auto-finalize.js +32 -2
- package/dist/lib/solo-auto-finalize.js.map +1 -1
- package/dist/lib/solo-auto-init-qa.d.ts.map +1 -1
- package/dist/lib/solo-auto-init-qa.js +3 -1
- package/dist/lib/solo-auto-init-qa.js.map +1 -1
- package/dist/lib/solo-auto-scout.d.ts +1 -0
- package/dist/lib/solo-auto-scout.d.ts.map +1 -1
- package/dist/lib/solo-auto-scout.js +44 -13
- package/dist/lib/solo-auto-scout.js.map +1 -1
- package/dist/lib/solo-auto-state.d.ts.map +1 -1
- package/dist/lib/solo-auto-state.js +88 -28
- package/dist/lib/solo-auto-state.js.map +1 -1
- package/dist/lib/solo-auto-types.d.ts +64 -3
- package/dist/lib/solo-auto-types.d.ts.map +1 -1
- package/dist/lib/solo-auto.d.ts +3 -3
- package/dist/lib/solo-auto.d.ts.map +1 -1
- package/dist/lib/solo-auto.js +83 -4
- package/dist/lib/solo-auto.js.map +1 -1
- package/dist/lib/solo-config.d.ts +51 -3
- package/dist/lib/solo-config.d.ts.map +1 -1
- package/dist/lib/solo-config.js +43 -2
- package/dist/lib/solo-config.js.map +1 -1
- package/dist/lib/solo-hints.d.ts +10 -0
- package/dist/lib/solo-hints.d.ts.map +1 -1
- package/dist/lib/solo-hints.js +22 -0
- package/dist/lib/solo-hints.js.map +1 -1
- package/dist/lib/solo-session-summary.d.ts +12 -1
- package/dist/lib/solo-session-summary.d.ts.map +1 -1
- package/dist/lib/solo-session-summary.js +50 -6
- package/dist/lib/solo-session-summary.js.map +1 -1
- package/dist/lib/spindle-incidents.d.ts +29 -0
- package/dist/lib/spindle-incidents.d.ts.map +1 -0
- package/dist/lib/spindle-incidents.js +56 -0
- package/dist/lib/spindle-incidents.js.map +1 -0
- package/dist/lib/spinner.d.ts +1 -1
- package/dist/lib/taste-profile.d.ts.map +1 -1
- package/dist/lib/taste-profile.js +14 -8
- package/dist/lib/taste-profile.js.map +1 -1
- package/dist/lib/ticket-steps/step-spindle.d.ts.map +1 -1
- package/dist/lib/ticket-steps/step-spindle.js +14 -0
- package/dist/lib/ticket-steps/step-spindle.js.map +1 -1
- package/dist/lib/trajectory-generate.d.ts +73 -0
- package/dist/lib/trajectory-generate.d.ts.map +1 -1
- package/dist/lib/trajectory-generate.js +368 -12
- package/dist/lib/trajectory-generate.js.map +1 -1
- package/dist/lib/trajectory.d.ts +1 -1
- package/dist/lib/trajectory.d.ts.map +1 -1
- package/dist/lib/trajectory.js +67 -6
- package/dist/lib/trajectory.js.map +1 -1
- package/dist/tui/screens/auto.d.ts +7 -0
- package/dist/tui/screens/auto.d.ts.map +1 -1
- package/dist/tui/screens/auto.js +18 -1
- package/dist/tui/screens/auto.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drill mode — auto-generate trajectories from scout output in spin mode.
|
|
3
|
+
*
|
|
4
|
+
* When drill mode is active and no trajectory is loaded, runs a broad survey
|
|
5
|
+
* scout, generates a trajectory from the proposals, and activates it. The spin
|
|
6
|
+
* loop then executes trajectory-guided cycles until the trajectory completes
|
|
7
|
+
* or stalls, at which point drill generates a fresh one.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { getPromptwheelDir } from './solo-config.js';
|
|
14
|
+
import { scoutAllSectors } from './solo-auto-planning.js';
|
|
15
|
+
import { generateTrajectoryFromProposals } from './trajectory-generate.js';
|
|
16
|
+
import { activateTrajectory, loadTrajectory, saveTrajectoryState } from './trajectory.js';
|
|
17
|
+
import { getNextStep as getTrajectoryNextStep, trajectoryComplete, } from '@promptwheel/core/trajectory/shared';
|
|
18
|
+
import { runMeasurement } from './goals.js';
|
|
19
|
+
import { formatTasteForPrompt } from './taste-profile.js';
|
|
20
|
+
import { formatLearningsForPrompt, selectRelevant } from './learnings.js';
|
|
21
|
+
import { formatDedupForPrompt } from './dedup-memory.js';
|
|
22
|
+
import { readHints as readHintsForDrill, writeHints as writeHintsForDrill } from './solo-hints.js';
|
|
23
|
+
// ── Adaptive cooldown ────────────────────────────────────────────────────────
|
|
24
|
+
const COOLDOWN_MIN_HISTORY = 3; // entries needed before adaptation kicks in
|
|
25
|
+
/**
|
|
26
|
+
* Compute cooldown cycles based on last trajectory outcome and historical success rate.
|
|
27
|
+
* First generation always returns 0. After that, applies adaptive tuning via a smooth
|
|
28
|
+
* sigmoid curve mapping recency-weighted completion rate to cooldown adjustment.
|
|
29
|
+
*
|
|
30
|
+
* Base cooldown uses granular completionPct from the last trajectory:
|
|
31
|
+
* - completionPct >= 0.8 → treated as completed (base = cooldownCompleted, default 0)
|
|
32
|
+
* - completionPct < 0.2 → treated as fully stalled (base = cooldownStalled, default 5)
|
|
33
|
+
* - Between → interpolated (e.g., 50% completion → ~2.5 base cooldown)
|
|
34
|
+
*
|
|
35
|
+
* Sigmoid adjustment (recency-weighted rate → adjustment):
|
|
36
|
+
* rate=0.0 → +4, rate=0.25 → +3, rate=0.5 → 0, rate=0.75 → -3, rate=1.0 → -4
|
|
37
|
+
*/
|
|
38
|
+
/** @internal Exported for testing. */
|
|
39
|
+
export function getDrillCooldown(state) {
|
|
40
|
+
if (state.drillTrajectoriesGenerated === 0)
|
|
41
|
+
return 0;
|
|
42
|
+
const drillConf = state.autoConf.drill;
|
|
43
|
+
const cooldownCompleted = drillConf?.cooldownCompleted ?? 0;
|
|
44
|
+
const cooldownStalled = drillConf?.cooldownStalled ?? 5;
|
|
45
|
+
// Step-1 failure override: immediate retry when failure rate is critical.
|
|
46
|
+
// Forces conservative ambition + zero cooldown to break the failure pattern.
|
|
47
|
+
const step1Critical = drillConf?.ambitionThresholds?.step1Critical ?? 0.4;
|
|
48
|
+
if (state.drillHistory.length >= COOLDOWN_MIN_HISTORY) {
|
|
49
|
+
const metrics = computeDrillMetrics(state.drillHistory);
|
|
50
|
+
if (metrics.step1FailureRate > step1Critical) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Use granular completionPct from last trajectory for interpolated base cooldown
|
|
55
|
+
const lastEntry = state.drillHistory.length > 0
|
|
56
|
+
? state.drillHistory[state.drillHistory.length - 1]
|
|
57
|
+
: undefined;
|
|
58
|
+
let baseCooldown;
|
|
59
|
+
if (!lastEntry && state.drillLastOutcome === null) {
|
|
60
|
+
// No history and unknown outcome → moderate default
|
|
61
|
+
baseCooldown = Math.round((cooldownCompleted + cooldownStalled) / 2);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const lastPct = lastEntry?.completionPct ?? (state.drillLastOutcome === 'completed' ? 1 : 0);
|
|
65
|
+
// Interpolate: pct=1.0 → cooldownCompleted, pct=0.0 → cooldownStalled
|
|
66
|
+
baseCooldown = Math.round(cooldownStalled + (cooldownCompleted - cooldownStalled) * lastPct);
|
|
67
|
+
}
|
|
68
|
+
// Smooth sigmoid adaptation based on recency-weighted success rate
|
|
69
|
+
if (state.drillHistory.length >= COOLDOWN_MIN_HISTORY) {
|
|
70
|
+
const rate = computeDrillMetrics(state.drillHistory).weightedCompletionRate;
|
|
71
|
+
// Configurable sigmoid parameters (via auto.json drill config)
|
|
72
|
+
const k = drillConf?.sigmoidK ?? 6;
|
|
73
|
+
const center = drillConf?.sigmoidCenter ?? 0.5;
|
|
74
|
+
// Sigmoid: smooth mapping from rate to adjustment
|
|
75
|
+
// rate=0 → +4 (more cooldown), rate=center → 0, rate=1.0 → -4 (less cooldown)
|
|
76
|
+
const sigmoid = 1 / (1 + Math.exp(-k * (rate - center)));
|
|
77
|
+
const adjustment = Math.round(4 - 8 * sigmoid);
|
|
78
|
+
// Freshness filter → cooldown bridge: adjust based on last generation's stale ratio.
|
|
79
|
+
// Many proposals stale → survey sooner. Very few stale → can wait longer.
|
|
80
|
+
let freshnessAdj = 0;
|
|
81
|
+
if (state.drillLastFreshnessDropRatio !== null) {
|
|
82
|
+
if (state.drillLastFreshnessDropRatio > 0.5) {
|
|
83
|
+
freshnessAdj = -2; // many proposals stale → survey sooner
|
|
84
|
+
}
|
|
85
|
+
else if (state.drillLastFreshnessDropRatio < 0.1) {
|
|
86
|
+
freshnessAdj = 1; // very few stale → can wait longer
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ±1 jitter prevents lockstep patterns in long sessions
|
|
90
|
+
const jitter = Math.round(Math.random() * 2) - 1; // -1, 0, or +1
|
|
91
|
+
return Math.max(0, baseCooldown + adjustment + freshnessAdj + jitter);
|
|
92
|
+
}
|
|
93
|
+
return baseCooldown;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Compute adaptive proposal thresholds based on historical success rate.
|
|
97
|
+
* Returns adjusted min/max proposals — scales with drill effectiveness.
|
|
98
|
+
*/
|
|
99
|
+
/** @internal Exported for testing. */
|
|
100
|
+
export function getAdaptiveProposalThresholds(state) {
|
|
101
|
+
const drillConf = state.autoConf.drill;
|
|
102
|
+
let min = drillConf?.minProposals ?? 3;
|
|
103
|
+
let max = drillConf?.maxProposals ?? 10;
|
|
104
|
+
if (state.drillHistory.length >= 3) {
|
|
105
|
+
const metrics = computeDrillMetrics(state.drillHistory);
|
|
106
|
+
if (metrics.weightedCompletionRate > 0.7) {
|
|
107
|
+
// High recent success — lower the bar (we generate good trajectories)
|
|
108
|
+
min = Math.max(2, min - 1);
|
|
109
|
+
max = max + 2;
|
|
110
|
+
}
|
|
111
|
+
else if (metrics.weightedCompletionRate < 0.3) {
|
|
112
|
+
// Low recent success — raise the bar (need better proposals)
|
|
113
|
+
min = min + 1;
|
|
114
|
+
max = Math.max(min + 1, max - 2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { min, max };
|
|
118
|
+
}
|
|
119
|
+
// ── Staleness detection ─────────────────────────────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* Measure code staleness since a given timestamp — gradient from 0.0 (completely fresh)
|
|
122
|
+
* to 1.0 (very stale, lots of changes). Returns the number of external commits as a
|
|
123
|
+
* normalized staleness score. Binary check: staleness > 0 means some changes exist.
|
|
124
|
+
*
|
|
125
|
+
* Uses git to detect commits after the timestamp, excluding PromptWheel's own
|
|
126
|
+
* commits (which don't represent external changes worth re-surveying for).
|
|
127
|
+
* Falls back to 1.0 (assume fully stale) if git is unavailable.
|
|
128
|
+
*/
|
|
129
|
+
function measureCodeStaleness(repoRoot, sinceTimestamp, logBase = 11) {
|
|
130
|
+
try {
|
|
131
|
+
// Validate timestamp — reject NaN, negative, or unreasonably old/future values
|
|
132
|
+
if (!Number.isFinite(sinceTimestamp) || sinceTimestamp <= 0)
|
|
133
|
+
return 1.0;
|
|
134
|
+
const sinceDate = new Date(sinceTimestamp).toISOString();
|
|
135
|
+
// Use --invert-grep to exclude commits authored by PromptWheel
|
|
136
|
+
const result = spawnSync('git', [
|
|
137
|
+
'log', '--oneline', `--since=${sinceDate}`,
|
|
138
|
+
'--invert-grep', '--grep=\\[promptwheel\\]', '--grep=Co-Authored-By: Claude',
|
|
139
|
+
'--', '.',
|
|
140
|
+
], {
|
|
141
|
+
cwd: repoRoot,
|
|
142
|
+
timeout: 5000,
|
|
143
|
+
encoding: 'utf-8',
|
|
144
|
+
});
|
|
145
|
+
if (result.error || result.status !== 0)
|
|
146
|
+
return 1.0; // assume stale on git error/timeout
|
|
147
|
+
const lines = result.stdout.trim();
|
|
148
|
+
if (!lines)
|
|
149
|
+
return 0.0;
|
|
150
|
+
const commitCount = lines.split('\n').length;
|
|
151
|
+
// Log scaling: diminishing returns after first few commits
|
|
152
|
+
// 1 commit ≈ 0.37, 3 commits ≈ 0.69, 5 commits ≈ 0.83, 10+ commits → ~1.0
|
|
153
|
+
return Math.min(1.0, Math.log(commitCount + 1) / Math.log(logBase));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return 1.0; // assume stale — better to survey unnecessarily than miss changes
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ── Trajectory history ───────────────────────────────────────────────────────
|
|
160
|
+
/** Build a summary of previous drill trajectories for the generation prompt. Capped to last 10 entries. */
|
|
161
|
+
export function formatDrillHistoryForPrompt(state) {
|
|
162
|
+
if (state.drillHistory.length === 0)
|
|
163
|
+
return '';
|
|
164
|
+
// Cap to recent entries to avoid bloating the LLM prompt
|
|
165
|
+
const MAX_HISTORY_IN_PROMPT = 10;
|
|
166
|
+
const recent = state.drillHistory.slice(-MAX_HISTORY_IN_PROMPT);
|
|
167
|
+
const skipped = state.drillHistory.length - recent.length;
|
|
168
|
+
const lines = recent.map((h, i) => {
|
|
169
|
+
const pct = h.completionPct !== undefined && h.completionPct !== null ? `${Math.round(h.completionPct * 100)}%` : `${h.stepsCompleted}/${h.stepsTotal}`;
|
|
170
|
+
const status = h.outcome === 'completed'
|
|
171
|
+
? `completed (${pct})`
|
|
172
|
+
: `stalled (${pct} done, ${h.stepsFailed} failed)`;
|
|
173
|
+
// Strip timestamp suffix from drill names for cleaner display
|
|
174
|
+
const displayName = h.name.replace(/-\d{13}$/, '');
|
|
175
|
+
let entry = `${skipped + i + 1}. "${displayName}" — ${h.description} [${status}]
|
|
176
|
+
Categories: ${h.categories.join(', ') || 'mixed'}
|
|
177
|
+
Scopes: ${h.scopes.join(', ') || 'broad'}`;
|
|
178
|
+
// Include specific failure details so LLM can learn from them
|
|
179
|
+
if (h.failedSteps && h.failedSteps.length > 0) {
|
|
180
|
+
const failDetails = h.failedSteps.map(f => `"${f.title}"${f.reason ? `: ${f.reason.slice(0, 100)}` : ''}`);
|
|
181
|
+
entry += `\n Failed steps: ${failDetails.join('; ')}`;
|
|
182
|
+
}
|
|
183
|
+
// Include completed step summaries for causal chaining
|
|
184
|
+
if (h.completedStepSummaries && h.completedStepSummaries.length > 0) {
|
|
185
|
+
entry += `\n Completed work: ${h.completedStepSummaries.slice(0, 3).join('; ')}`;
|
|
186
|
+
}
|
|
187
|
+
// Include modified files so next trajectory can build on them
|
|
188
|
+
if (h.modifiedFiles && h.modifiedFiles.length > 0) {
|
|
189
|
+
const files = h.modifiedFiles.slice(0, 8);
|
|
190
|
+
entry += `\n Modified files: ${files.join(', ')}${h.modifiedFiles.length > 8 ? ` (+${h.modifiedFiles.length - 8} more)` : ''}`;
|
|
191
|
+
}
|
|
192
|
+
return entry;
|
|
193
|
+
});
|
|
194
|
+
if (skipped > 0) {
|
|
195
|
+
lines.unshift(`(${skipped} older trajectories omitted)`);
|
|
196
|
+
}
|
|
197
|
+
return lines.join('\n');
|
|
198
|
+
}
|
|
199
|
+
// ── Theme diversity ──────────────────────────────────────────────────────────
|
|
200
|
+
/**
|
|
201
|
+
* Compute recency-weighted coverage counts from drill history.
|
|
202
|
+
* Recent trajectories contribute more to coverage than old ones, so categories
|
|
203
|
+
* explored 20+ trajectories ago get fresh exploration scores again.
|
|
204
|
+
* Uses exponential decay with half-life of 10 entries.
|
|
205
|
+
*
|
|
206
|
+
* @internal Exported for testing.
|
|
207
|
+
*/
|
|
208
|
+
export function computeDecayedCoverage(history) {
|
|
209
|
+
const DECAY_LAMBDA = Math.LN2 / 10; // half-life of 10 entries
|
|
210
|
+
const categories = new Map();
|
|
211
|
+
const scopes = new Map();
|
|
212
|
+
for (let i = 0; i < history.length; i++) {
|
|
213
|
+
const h = history[i];
|
|
214
|
+
const age = history.length - 1 - i; // 0 = newest
|
|
215
|
+
const weight = Math.exp(-DECAY_LAMBDA * age);
|
|
216
|
+
for (const cat of h.categories) {
|
|
217
|
+
categories.set(cat, (categories.get(cat) ?? 0) + weight);
|
|
218
|
+
}
|
|
219
|
+
for (const scope of h.scopes) {
|
|
220
|
+
scopes.set(scope, (scopes.get(scope) ?? 0) + weight);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { categories, scopes };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Build a diversity hint with numeric exploration scores for the generation prompt.
|
|
227
|
+
* Score = 1/(decayedCount + 1) — higher score means less explored (preferred).
|
|
228
|
+
* Uses recency-weighted counts so old coverage fades over time.
|
|
229
|
+
*
|
|
230
|
+
* All standard categories are listed so the LLM knows the full palette.
|
|
231
|
+
*/
|
|
232
|
+
function formatDiversityHint(state) {
|
|
233
|
+
if (state.drillHistory.length === 0 && state.drillCoveredCategories.size === 0 && state.drillCoveredScopes.size === 0)
|
|
234
|
+
return '';
|
|
235
|
+
const parts = [];
|
|
236
|
+
const allCategories = ['security', 'fix', 'perf', 'refactor', 'test', 'types', 'cleanup', 'docs'];
|
|
237
|
+
// Use decayed counts from history when available, fall back to raw cumulative counts
|
|
238
|
+
const decayed = state.drillHistory.length > 0
|
|
239
|
+
? computeDecayedCoverage(state.drillHistory)
|
|
240
|
+
: { categories: state.drillCoveredCategories, scopes: state.drillCoveredScopes };
|
|
241
|
+
// Category diversity scores (decayed)
|
|
242
|
+
const catScores = allCategories.map(cat => {
|
|
243
|
+
const count = decayed.categories.get(cat) ?? 0;
|
|
244
|
+
const score = (1 / (count + 1)).toFixed(2);
|
|
245
|
+
const label = count > 0 ? ` (${count.toFixed(1)} effective coverage)` : ' (unexplored)';
|
|
246
|
+
return `${cat}: ${score}${label}`;
|
|
247
|
+
});
|
|
248
|
+
parts.push(`Category exploration scores (higher = less explored, PREFER higher scores):\n${catScores.join('\n')}`);
|
|
249
|
+
// Scope diversity scores (decayed, capped to top 20)
|
|
250
|
+
if (decayed.scopes.size > 0) {
|
|
251
|
+
const MAX_SCOPES_IN_PROMPT = 20;
|
|
252
|
+
const sorted = [...decayed.scopes.entries()]
|
|
253
|
+
.sort((a, b) => b[1] - a[1])
|
|
254
|
+
.slice(0, MAX_SCOPES_IN_PROMPT);
|
|
255
|
+
const scopeScores = sorted.map(([s, n]) => `${s}: ${(1 / (n + 1)).toFixed(2)} (${n.toFixed(1)} effective)`);
|
|
256
|
+
if (decayed.scopes.size > MAX_SCOPES_IN_PROMPT) {
|
|
257
|
+
scopeScores.push(`(${decayed.scopes.size - MAX_SCOPES_IN_PROMPT} more omitted)`);
|
|
258
|
+
}
|
|
259
|
+
parts.push(`Scope exploration scores:\n${scopeScores.join('\n')}`);
|
|
260
|
+
}
|
|
261
|
+
return parts.join('\n\n');
|
|
262
|
+
}
|
|
263
|
+
// ── Sector context ───────────────────────────────────────────────────────────
|
|
264
|
+
/** Build a sector summary for the generation prompt. Capped to top 30 sectors by file count. */
|
|
265
|
+
export function formatSectorContextForPrompt(state) {
|
|
266
|
+
if (!state.sectorState)
|
|
267
|
+
return '';
|
|
268
|
+
const MAX_SECTORS_IN_PROMPT = 30;
|
|
269
|
+
const production = state.sectorState.sectors.filter(s => s.production && s.fileCount > 0);
|
|
270
|
+
// Sort by file count descending to prioritize significant sectors
|
|
271
|
+
const sorted = [...production].sort((a, b) => b.fileCount - a.fileCount);
|
|
272
|
+
const capped = sorted.slice(0, MAX_SECTORS_IN_PROMPT);
|
|
273
|
+
const sectors = capped.map(s => {
|
|
274
|
+
const scanLabel = s.scanCount > 0
|
|
275
|
+
? `scanned ${s.scanCount}x, yield ${s.proposalYield}`
|
|
276
|
+
: 'unscanned';
|
|
277
|
+
return `- ${s.path} (${s.fileCount} files, ${scanLabel})`;
|
|
278
|
+
});
|
|
279
|
+
if (sectors.length === 0)
|
|
280
|
+
return '';
|
|
281
|
+
if (production.length > MAX_SECTORS_IN_PROMPT) {
|
|
282
|
+
sectors.push(`(${production.length - MAX_SECTORS_IN_PROMPT} smaller sectors omitted)`);
|
|
283
|
+
}
|
|
284
|
+
return sectors.join('\n');
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Compute drill health metrics from history.
|
|
288
|
+
* Uses recency-weighted exponential decay (half-life ~5 entries) so recent
|
|
289
|
+
* outcomes outweigh old ones in completion rates and category success.
|
|
290
|
+
*/
|
|
291
|
+
export function computeDrillMetrics(history) {
|
|
292
|
+
if (history.length === 0) {
|
|
293
|
+
return {
|
|
294
|
+
totalTrajectories: 0, completionRate: 0, weightedCompletionRate: 0,
|
|
295
|
+
avgStepCompletionRate: 0, weightedStepCompletionRate: 0,
|
|
296
|
+
avgStepsPerTrajectory: 0, categorySuccessRates: {}, topCategories: [], stalledCategories: [],
|
|
297
|
+
step1FailureRate: 0, stepPositionFailureRates: [],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// Recency decay: λ = ln(2)/5 ≈ 0.1386 → half-life of 5 entries
|
|
301
|
+
const DECAY_LAMBDA = Math.LN2 / 5;
|
|
302
|
+
const completed = history.filter(h => h.outcome === 'completed').length;
|
|
303
|
+
const completionRate = completed / history.length;
|
|
304
|
+
// Recency-weighted completion rate (binary: completed=1, stalled=0)
|
|
305
|
+
let weightedSum = 0;
|
|
306
|
+
let weightTotal = 0;
|
|
307
|
+
// Recency-weighted step completion rate (granular: completionPct 0-1)
|
|
308
|
+
let weightedStepSum = 0;
|
|
309
|
+
let weightedStepTotal = 0;
|
|
310
|
+
let totalSteps = 0;
|
|
311
|
+
let totalStepsCompleted = 0;
|
|
312
|
+
for (let i = 0; i < history.length; i++) {
|
|
313
|
+
const h = history[i];
|
|
314
|
+
const age = history.length - 1 - i; // 0 = newest, N-1 = oldest
|
|
315
|
+
const weight = Math.exp(-DECAY_LAMBDA * age);
|
|
316
|
+
totalSteps += h.stepsTotal;
|
|
317
|
+
totalStepsCompleted += h.stepsCompleted;
|
|
318
|
+
weightedSum += (h.outcome === 'completed' ? 1 : 0) * weight;
|
|
319
|
+
weightTotal += weight;
|
|
320
|
+
// Use granular completionPct if available, fall back to stepsCompleted/stepsTotal
|
|
321
|
+
const pct = h.completionPct ?? (h.stepsTotal > 0 ? h.stepsCompleted / h.stepsTotal : 0);
|
|
322
|
+
weightedStepSum += pct * weight;
|
|
323
|
+
weightedStepTotal += weight;
|
|
324
|
+
}
|
|
325
|
+
const weightedCompletionRate = weightTotal > 0 ? weightedSum / weightTotal : 0;
|
|
326
|
+
const weightedStepCompletionRate = weightedStepTotal > 0 ? weightedStepSum / weightedStepTotal : 0;
|
|
327
|
+
const avgStepCompletionRate = totalSteps > 0 ? totalStepsCompleted / totalSteps : 0;
|
|
328
|
+
const avgStepsPerTrajectory = history.length > 0 ? totalSteps / history.length : 0;
|
|
329
|
+
// Category success rates with recency weighting
|
|
330
|
+
const catStats = {};
|
|
331
|
+
for (let i = 0; i < history.length; i++) {
|
|
332
|
+
const h = history[i];
|
|
333
|
+
const age = history.length - 1 - i;
|
|
334
|
+
const weight = Math.exp(-DECAY_LAMBDA * age);
|
|
335
|
+
for (const cat of h.categories) {
|
|
336
|
+
const s = catStats[cat] ??= { completed: 0, total: 0, weightedCompleted: 0, weightedTotal: 0 };
|
|
337
|
+
s.total++;
|
|
338
|
+
s.weightedTotal += weight;
|
|
339
|
+
if (h.outcome === 'completed') {
|
|
340
|
+
s.completed++;
|
|
341
|
+
s.weightedCompleted += weight;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const categorySuccessRates = {};
|
|
346
|
+
for (const [cat, s] of Object.entries(catStats)) {
|
|
347
|
+
// Use weighted rate for ranking, store raw counts for reporting
|
|
348
|
+
const rate = s.weightedTotal > 0 ? s.weightedCompleted / s.weightedTotal : 0;
|
|
349
|
+
categorySuccessRates[cat] = { completed: s.completed, total: s.total, rate };
|
|
350
|
+
}
|
|
351
|
+
const sorted = Object.entries(categorySuccessRates).sort((a, b) => b[1].rate - a[1].rate);
|
|
352
|
+
const topCategories = sorted.filter(([, s]) => s.rate >= 0.5).map(([c]) => c);
|
|
353
|
+
const stalledCategories = sorted.filter(([, s]) => s.rate < 0.3 && s.total >= 2).map(([c]) => c);
|
|
354
|
+
// Step-1 failure rate: trajectories that stalled with 0 steps completed
|
|
355
|
+
const step1Failures = history.filter(h => h.outcome === 'stalled' && h.stepsCompleted === 0).length;
|
|
356
|
+
const step1FailureRate = step1Failures / history.length;
|
|
357
|
+
// Step-position failure analysis: aggregate stepOutcomes by position
|
|
358
|
+
const positionStats = [];
|
|
359
|
+
for (const h of history) {
|
|
360
|
+
if (!h.stepOutcomes)
|
|
361
|
+
continue;
|
|
362
|
+
for (let pos = 0; pos < h.stepOutcomes.length; pos++) {
|
|
363
|
+
if (!positionStats[pos])
|
|
364
|
+
positionStats[pos] = { failed: 0, total: 0 };
|
|
365
|
+
positionStats[pos].total++;
|
|
366
|
+
if (h.stepOutcomes[pos].status === 'failed')
|
|
367
|
+
positionStats[pos].failed++;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const stepPositionFailureRates = positionStats
|
|
371
|
+
.map((s, i) => ({ position: i + 1, failureRate: s.total > 0 ? s.failed / s.total : 0, total: s.total }))
|
|
372
|
+
.filter(s => s.total >= 2); // need at least 2 data points
|
|
373
|
+
return {
|
|
374
|
+
totalTrajectories: history.length,
|
|
375
|
+
completionRate,
|
|
376
|
+
weightedCompletionRate,
|
|
377
|
+
avgStepCompletionRate,
|
|
378
|
+
weightedStepCompletionRate,
|
|
379
|
+
avgStepsPerTrajectory,
|
|
380
|
+
categorySuccessRates,
|
|
381
|
+
topCategories,
|
|
382
|
+
stalledCategories,
|
|
383
|
+
step1FailureRate,
|
|
384
|
+
stepPositionFailureRates,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Compute an ambition level from session metrics. Scales first-step complexity
|
|
389
|
+
* dynamically based on weighted completion rate and step-1 failure rate.
|
|
390
|
+
*
|
|
391
|
+
* Includes fast-recovery: if the last 2 trajectories both completed, bump up
|
|
392
|
+
* one level regardless of overall weighted rate. This prevents the system from
|
|
393
|
+
* being stuck at conservative after a string of old failures.
|
|
394
|
+
*/
|
|
395
|
+
export function computeAmbitionLevel(state) {
|
|
396
|
+
if (state.drillHistory.length < 3)
|
|
397
|
+
return 'conservative';
|
|
398
|
+
const drillConf = state.autoConf.drill;
|
|
399
|
+
const metrics = computeDrillMetrics(state.drillHistory);
|
|
400
|
+
// Configurable thresholds (via auto.json drill.ambitionThresholds)
|
|
401
|
+
const step1Critical = drillConf?.ambitionThresholds?.step1Critical ?? 0.4;
|
|
402
|
+
const step1Threshold = drillConf?.ambitionThresholds?.step1Fail ?? 0.25;
|
|
403
|
+
const lowCompletion = drillConf?.ambitionThresholds?.conservative ?? 0.3;
|
|
404
|
+
const highCompletion = drillConf?.ambitionThresholds?.ambitious ?? 0.7;
|
|
405
|
+
const step1AmbitiousMax = drillConf?.ambitionThresholds?.step1AmbitiousMax ?? 0.15;
|
|
406
|
+
// Critical step-1 failure: ALWAYS conservative, no fast-recovery override.
|
|
407
|
+
// This breaks the "generate bad trajectory → stall on step 1 → repeat" cycle.
|
|
408
|
+
if (metrics.step1FailureRate > step1Critical)
|
|
409
|
+
return 'conservative';
|
|
410
|
+
// Fast-recovery: 2 consecutive completions → bump up one level
|
|
411
|
+
const last2 = state.drillHistory.slice(-2);
|
|
412
|
+
const consecutiveWins = last2.length === 2 && last2.every(h => h.outcome === 'completed');
|
|
413
|
+
// Per-ambition success tracking: if ambitious has been tried and fails too often, stay moderate
|
|
414
|
+
const ambitionRates = computePerAmbitionSuccessRates(state.drillHistory);
|
|
415
|
+
if (metrics.step1FailureRate > step1Threshold || metrics.weightedCompletionRate < lowCompletion) {
|
|
416
|
+
return consecutiveWins ? 'moderate' : 'conservative';
|
|
417
|
+
}
|
|
418
|
+
if (metrics.weightedCompletionRate > highCompletion && metrics.step1FailureRate < step1AmbitiousMax && state.drillHistory.length >= 5) {
|
|
419
|
+
// Per-ambition guard: if ambitious trajectories have < 40% success, stay moderate
|
|
420
|
+
if (ambitionRates.ambitious !== null && ambitionRates.ambitious < 0.4)
|
|
421
|
+
return 'moderate';
|
|
422
|
+
return 'ambitious';
|
|
423
|
+
}
|
|
424
|
+
// Fast-recovery from moderate → ambitious on streak
|
|
425
|
+
if (consecutiveWins && metrics.step1FailureRate < step1AmbitiousMax && state.drillHistory.length >= 4) {
|
|
426
|
+
if (ambitionRates.ambitious !== null && ambitionRates.ambitious < 0.4)
|
|
427
|
+
return 'moderate';
|
|
428
|
+
return 'ambitious';
|
|
429
|
+
}
|
|
430
|
+
return 'moderate';
|
|
431
|
+
}
|
|
432
|
+
/** Compute success rate per ambition level from history. Returns null if fewer than 2 entries for a level. */
|
|
433
|
+
export function computePerAmbitionSuccessRates(history) {
|
|
434
|
+
const counts = {
|
|
435
|
+
conservative: { ok: 0, total: 0 },
|
|
436
|
+
moderate: { ok: 0, total: 0 },
|
|
437
|
+
ambitious: { ok: 0, total: 0 },
|
|
438
|
+
};
|
|
439
|
+
for (const h of history) {
|
|
440
|
+
const level = h.ambitionLevel;
|
|
441
|
+
if (level && counts[level]) {
|
|
442
|
+
counts[level].total++;
|
|
443
|
+
if (h.outcome === 'completed')
|
|
444
|
+
counts[level].ok++;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
conservative: counts.conservative.total >= 2 ? counts.conservative.ok / counts.conservative.total : null,
|
|
449
|
+
moderate: counts.moderate.total >= 2 ? counts.moderate.ok / counts.moderate.total : null,
|
|
450
|
+
ambitious: counts.ambitious.total >= 2 ? counts.ambitious.ok / counts.ambitious.total : null,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
// ── Multi-trajectory arc guidance ─────────────────────────────────────────────
|
|
454
|
+
/**
|
|
455
|
+
* Analyze the category distribution and outcomes of recent trajectories to
|
|
456
|
+
* provide directional guidance for the next one — shifting focus from
|
|
457
|
+
* foundation to core to polish, pivoting away from stalled areas, and
|
|
458
|
+
* building on successful momentum.
|
|
459
|
+
*
|
|
460
|
+
* Uses primary category (first in list) for phase detection to avoid
|
|
461
|
+
* double-counting trajectories that span foundation and polish categories.
|
|
462
|
+
*
|
|
463
|
+
* When a goalCategory is provided, biases guidance toward categories that
|
|
464
|
+
* advance the active goal rather than purely phase-rotating.
|
|
465
|
+
*
|
|
466
|
+
* ## Equilibrium properties
|
|
467
|
+
*
|
|
468
|
+
* Signals are prioritized and capped at 2 to prevent contradictory advice.
|
|
469
|
+
* Priority order: stall pivot > phase rotation > momentum > chain > goal.
|
|
470
|
+
* Stall pivot and momentum are mutually exclusive — when both would fire,
|
|
471
|
+
* a blended "selective momentum" signal is emitted instead. Chain guidance
|
|
472
|
+
* is suppressed when momentum already fires (avoids double "continue" signal).
|
|
473
|
+
* Goal alignment is reweighted (not suppressed) when the phase rotation already
|
|
474
|
+
* directs toward the goal category — a softer nudge ensures the goal gets
|
|
475
|
+
* priority within the shift rather than being silenced entirely.
|
|
476
|
+
*/
|
|
477
|
+
export function computeArcGuidance(state, goalCategory) {
|
|
478
|
+
if (state.drillHistory.length < 2)
|
|
479
|
+
return undefined;
|
|
480
|
+
const MAX_SIGNALS = 2;
|
|
481
|
+
const recentWindow = state.drillHistory.slice(-5);
|
|
482
|
+
const parts = [];
|
|
483
|
+
// Use primary category (first in list) per trajectory — avoids double-counting
|
|
484
|
+
// trajectories like ['refactor', 'test'] as both foundation and polish
|
|
485
|
+
const primaryCats = recentWindow.map(h => h.categories[0] ?? 'other');
|
|
486
|
+
// Count primary category distribution
|
|
487
|
+
const catCounts = new Map();
|
|
488
|
+
for (const cat of primaryCats) {
|
|
489
|
+
catCounts.set(cat, (catCounts.get(cat) ?? 0) + 1);
|
|
490
|
+
}
|
|
491
|
+
const dominant = [...catCounts.entries()]
|
|
492
|
+
.sort((a, b) => b[1] - a[1])
|
|
493
|
+
.slice(0, 2)
|
|
494
|
+
.map(([c]) => c);
|
|
495
|
+
// Phase detection: foundation → core → polish (using primary category only)
|
|
496
|
+
const foundationCats = new Set(['types', 'refactor', 'fix']);
|
|
497
|
+
const polishCats = new Set(['test', 'docs', 'cleanup']);
|
|
498
|
+
const foundationCount = primaryCats.filter(c => foundationCats.has(c)).length;
|
|
499
|
+
const polishCount = primaryCats.filter(c => polishCats.has(c)).length;
|
|
500
|
+
// Track which direction phase rotation suggests (used for goal dedup)
|
|
501
|
+
let phaseDirection = null;
|
|
502
|
+
// Stall and momentum signals — compute both, then resolve conflicts
|
|
503
|
+
const stalledInWindow = recentWindow.filter(h => h.outcome === 'stalled');
|
|
504
|
+
const completedInWindow = recentWindow.filter(h => h.outcome === 'completed');
|
|
505
|
+
const hasStalls = stalledInWindow.length >= 2;
|
|
506
|
+
const hasMomentum = completedInWindow.length >= 3;
|
|
507
|
+
// Priority 1: Stall pivot (highest priority — avoiding repeated failure is critical)
|
|
508
|
+
// When both stalls and momentum fire, emit a blended signal instead of both
|
|
509
|
+
const allStdCategories = ['security', 'fix', 'perf', 'refactor', 'test', 'types', 'cleanup', 'docs'];
|
|
510
|
+
if (hasStalls && hasMomentum) {
|
|
511
|
+
const stalledCats = [...new Set(stalledInWindow.flatMap(h => h.categories))];
|
|
512
|
+
const completedCats = [...new Set(completedInWindow.flatMap(h => h.categories))];
|
|
513
|
+
const touchedCats = new Set([...stalledCats, ...completedCats]);
|
|
514
|
+
const unexplored = allStdCategories.filter(c => !touchedCats.has(c));
|
|
515
|
+
const diversityNudge = unexplored.length > 0
|
|
516
|
+
? ` Also consider unexplored categories (${unexplored.slice(0, 3).join(', ')}) to diversify.`
|
|
517
|
+
: '';
|
|
518
|
+
parts.push(`Selective momentum: strong completions in ${completedCats.join(', ')} but repeated stalls in ${stalledCats.join(', ')}. Double down on successful categories and avoid stalled areas.${diversityNudge}`);
|
|
519
|
+
}
|
|
520
|
+
else if (hasStalls) {
|
|
521
|
+
const stalledCats = [...new Set(stalledInWindow.flatMap(h => h.categories))];
|
|
522
|
+
parts.push(`Multiple recent stalls in: ${stalledCats.join(', ')}. Pivot to a completely different area or category.`);
|
|
523
|
+
}
|
|
524
|
+
// Priority 2: Phase rotation (only if budget remains)
|
|
525
|
+
if (parts.length < MAX_SIGNALS) {
|
|
526
|
+
if (foundationCount >= 3 && polishCount < 2) {
|
|
527
|
+
phaseDirection = 'polish';
|
|
528
|
+
parts.push(`Recent trajectories focused on foundation work (${dominant.join(', ')}). Shift toward testing and documentation — write tests for recently refactored code, validate new interfaces, add missing docs.`);
|
|
529
|
+
}
|
|
530
|
+
else if (polishCount >= 3) {
|
|
531
|
+
phaseDirection = 'core';
|
|
532
|
+
parts.push(`Recent trajectories focused on polish (${dominant.join(', ')}). Shift to core improvements — security hardening, performance optimization, or deeper refactors.`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Priority 3: Momentum (only when stall pivot didn't fire — they're mutually exclusive)
|
|
536
|
+
if (parts.length < MAX_SIGNALS && hasMomentum && !hasStalls) {
|
|
537
|
+
parts.push(`Strong completion momentum (${completedInWindow.length}/${recentWindow.length} completed). Build on this — tackle slightly more ambitious work in the same areas, or expand to adjacent modules.`);
|
|
538
|
+
}
|
|
539
|
+
// Priority 4: Chain guidance (only when momentum didn't fire — avoids double "continue" signal)
|
|
540
|
+
if (parts.length < MAX_SIGNALS && !hasMomentum) {
|
|
541
|
+
const last = recentWindow[recentWindow.length - 1];
|
|
542
|
+
if (last?.outcome === 'completed' && last.modifiedFiles && last.modifiedFiles.length > 0) {
|
|
543
|
+
const areas = [...new Set(last.modifiedFiles.map(f => f.split('/').slice(0, -1).join('/')))].slice(0, 3);
|
|
544
|
+
parts.push(`Last trajectory modified ${areas.join(', ')}. Consider a follow-up trajectory that adds tests, improves docs, or extends functionality in those same areas.`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Priority 5: Goal alignment — always emits when goal exists and budget remains,
|
|
548
|
+
// but reweights the message when phase rotation already covers the goal category
|
|
549
|
+
// (softer nudge instead of full suppression to prevent goal drift)
|
|
550
|
+
if (parts.length < MAX_SIGNALS && goalCategory) {
|
|
551
|
+
const phaseCoversGoal = (phaseDirection === 'polish' && polishCats.has(goalCategory)) ||
|
|
552
|
+
(phaseDirection === 'core' && !foundationCats.has(goalCategory) && !polishCats.has(goalCategory));
|
|
553
|
+
const goalCatCount = primaryCats.filter(c => c === goalCategory).length;
|
|
554
|
+
if (phaseCoversGoal) {
|
|
555
|
+
// Phase rotation already points toward goal — softer nudge to ensure priority
|
|
556
|
+
parts.push(`Phase shift aligns with active goal ("${goalCategory}") — ensure "${goalCategory}" gets priority within the shift rather than other ${phaseDirection === 'polish' ? 'polish' : 'core'} categories.`);
|
|
557
|
+
}
|
|
558
|
+
else if (goalCatCount < 2) {
|
|
559
|
+
parts.push(`Active goal targets "${goalCategory}" work — prioritize proposals in this category to advance the goal.`);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
parts.push(`Good goal alignment — recent trajectories include "${goalCategory}" work. Continue advancing the goal while maintaining category diversity.`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return parts.length > 0 ? parts.join('\n') : undefined;
|
|
566
|
+
}
|
|
567
|
+
function getDrillHistoryPath(repoRoot) {
|
|
568
|
+
return path.join(getPromptwheelDir(repoRoot), 'drill-history.json');
|
|
569
|
+
}
|
|
570
|
+
/** Load persisted drill history from disk. Returns empty state if missing or corrupted. */
|
|
571
|
+
export function loadDrillHistory(repoRoot) {
|
|
572
|
+
const empty = { entries: [], coveredCategories: {}, coveredScopes: {} };
|
|
573
|
+
try {
|
|
574
|
+
const filePath = getDrillHistoryPath(repoRoot);
|
|
575
|
+
const tmpPath = filePath + '.tmp';
|
|
576
|
+
// Recover orphaned .tmp file from crash — read .tmp first, validate, then promote
|
|
577
|
+
if (!fs.existsSync(filePath) && fs.existsSync(tmpPath)) {
|
|
578
|
+
try {
|
|
579
|
+
const tmpRaw = fs.readFileSync(tmpPath, 'utf-8');
|
|
580
|
+
const tmpData = JSON.parse(tmpRaw);
|
|
581
|
+
if (tmpData && typeof tmpData === 'object' && Array.isArray(tmpData.entries)) {
|
|
582
|
+
fs.renameSync(tmpPath, filePath);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
// Invalid .tmp — remove it
|
|
586
|
+
fs.unlinkSync(tmpPath);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// .tmp is corrupted — remove it
|
|
591
|
+
try {
|
|
592
|
+
fs.unlinkSync(tmpPath);
|
|
593
|
+
}
|
|
594
|
+
catch { /* ignore */ }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (!fs.existsSync(filePath))
|
|
598
|
+
return empty;
|
|
599
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
600
|
+
if (!raw.trim())
|
|
601
|
+
return empty;
|
|
602
|
+
const data = JSON.parse(raw);
|
|
603
|
+
if (!data || typeof data !== 'object')
|
|
604
|
+
return empty;
|
|
605
|
+
return {
|
|
606
|
+
entries: Array.isArray(data.entries) ? data.entries : [],
|
|
607
|
+
coveredCategories: (data.coveredCategories && typeof data.coveredCategories === 'object' && !Array.isArray(data.coveredCategories))
|
|
608
|
+
? data.coveredCategories : {},
|
|
609
|
+
coveredScopes: (data.coveredScopes && typeof data.coveredScopes === 'object' && !Array.isArray(data.coveredScopes))
|
|
610
|
+
? data.coveredScopes : {},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
console.log(chalk.yellow(` Drill: history corrupted — starting fresh (${err instanceof Error ? err.message : String(err)})`));
|
|
615
|
+
return empty;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/** Persist drill history to disk. Caps entries to prevent unbounded growth. */
|
|
619
|
+
function saveDrillHistory(repoRoot, history, cap = 100) {
|
|
620
|
+
const filePath = getDrillHistoryPath(repoRoot);
|
|
621
|
+
const tmp = filePath + '.tmp';
|
|
622
|
+
try {
|
|
623
|
+
const validCap = Math.max(10, Math.min(1000, cap));
|
|
624
|
+
const capped = { ...history, entries: history.entries.slice(-validCap) };
|
|
625
|
+
fs.writeFileSync(tmp, JSON.stringify(capped, null, 2));
|
|
626
|
+
fs.renameSync(tmp, filePath);
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
console.log(chalk.yellow(` Drill: failed to save history (${err instanceof Error ? err.message : String(err)})`));
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
// Always clean up .tmp to prevent orphaned files on error
|
|
633
|
+
try {
|
|
634
|
+
if (fs.existsSync(tmp))
|
|
635
|
+
fs.unlinkSync(tmp);
|
|
636
|
+
}
|
|
637
|
+
catch { /* ignore */ }
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/** Hydrate session state from persisted drill history. */
|
|
641
|
+
export function hydrateDrillState(state) {
|
|
642
|
+
const persisted = loadDrillHistory(state.repoRoot);
|
|
643
|
+
if (persisted.entries.length === 0)
|
|
644
|
+
return;
|
|
645
|
+
state.drillHistory = persisted.entries;
|
|
646
|
+
state.drillCoveredCategories = new Map(Object.entries(persisted.coveredCategories));
|
|
647
|
+
state.drillCoveredScopes = new Map(Object.entries(persisted.coveredScopes));
|
|
648
|
+
// Restore generation count from history so cooldown works correctly on resume
|
|
649
|
+
state.drillTrajectoriesGenerated = persisted.entries.length;
|
|
650
|
+
// Set last outcome from most recent entry
|
|
651
|
+
const last = persisted.entries[persisted.entries.length - 1];
|
|
652
|
+
if (last)
|
|
653
|
+
state.drillLastOutcome = last.outcome;
|
|
654
|
+
}
|
|
655
|
+
// ── Track trajectory outcome ─────────────────────────────────────────────────
|
|
656
|
+
/**
|
|
657
|
+
* Record a completed or stalled drill trajectory into history.
|
|
658
|
+
* Called from between-cycles when a trajectory finishes.
|
|
659
|
+
* Persists to disk for cross-session diversity.
|
|
660
|
+
*/
|
|
661
|
+
export function recordDrillTrajectoryOutcome(state, trajectoryName, trajectoryDescription, stepsTotal, stepsCompleted, stepsFailed, outcome, steps, failedSteps, completedStepSummaries, modifiedFiles, ambitionLevel, telemetry) {
|
|
662
|
+
// Collect categories and scopes from all steps
|
|
663
|
+
const categories = [...new Set(steps.flatMap(s => s.categories ?? []))];
|
|
664
|
+
const scopes = [...new Set(steps.map(s => s.scope).filter((s) => !!s))];
|
|
665
|
+
// Cap in-memory history before push to prevent unbounded growth
|
|
666
|
+
const historyCap = state.autoConf.drill?.historyCap ?? 100;
|
|
667
|
+
const validCap = Math.max(10, Math.min(1000, historyCap));
|
|
668
|
+
if (state.drillHistory.length >= validCap) {
|
|
669
|
+
state.drillHistory = state.drillHistory.slice(-(validCap - 1));
|
|
670
|
+
}
|
|
671
|
+
state.drillHistory.push({
|
|
672
|
+
name: trajectoryName,
|
|
673
|
+
description: trajectoryDescription,
|
|
674
|
+
stepsTotal,
|
|
675
|
+
stepsCompleted,
|
|
676
|
+
stepsFailed,
|
|
677
|
+
outcome,
|
|
678
|
+
completionPct: stepsTotal > 0 ? stepsCompleted / stepsTotal : 0,
|
|
679
|
+
categories,
|
|
680
|
+
scopes,
|
|
681
|
+
timestamp: Date.now(),
|
|
682
|
+
failedSteps: failedSteps?.slice(0, 5), // cap to prevent unbounded growth
|
|
683
|
+
completedStepSummaries: completedStepSummaries?.slice(0, 5),
|
|
684
|
+
modifiedFiles: modifiedFiles?.slice(0, 20), // cap to prevent unbounded growth
|
|
685
|
+
ambitionLevel,
|
|
686
|
+
// Telemetry — enables empirical analysis of edge cases
|
|
687
|
+
stepOutcomes: telemetry?.stepOutcomes?.slice(0, 10),
|
|
688
|
+
proposalAvgConfidence: telemetry?.proposalAvgConfidence,
|
|
689
|
+
proposalAvgImpact: telemetry?.proposalAvgImpact,
|
|
690
|
+
freshnessDropCount: telemetry?.freshnessDropCount,
|
|
691
|
+
proposalCategoryCount: telemetry?.proposalCategoryCount,
|
|
692
|
+
});
|
|
693
|
+
state.drillLastOutcome = outcome;
|
|
694
|
+
// Update diversity tracking
|
|
695
|
+
for (const cat of categories) {
|
|
696
|
+
state.drillCoveredCategories.set(cat, (state.drillCoveredCategories.get(cat) ?? 0) + 1);
|
|
697
|
+
}
|
|
698
|
+
for (const scope of scopes) {
|
|
699
|
+
state.drillCoveredScopes.set(scope, (state.drillCoveredScopes.get(scope) ?? 0) + 1);
|
|
700
|
+
}
|
|
701
|
+
// Cap scopes map to prevent unbounded growth in monorepos
|
|
702
|
+
const MAX_TRACKED_SCOPES = 200;
|
|
703
|
+
if (state.drillCoveredScopes.size > MAX_TRACKED_SCOPES) {
|
|
704
|
+
// Keep top entries by frequency, prune the rest
|
|
705
|
+
const sorted = [...state.drillCoveredScopes.entries()].sort((a, b) => b[1] - a[1]);
|
|
706
|
+
state.drillCoveredScopes = new Map(sorted.slice(0, MAX_TRACKED_SCOPES));
|
|
707
|
+
}
|
|
708
|
+
// Persist to disk for cross-session awareness
|
|
709
|
+
const drillConf = state.autoConf.drill;
|
|
710
|
+
saveDrillHistory(state.repoRoot, {
|
|
711
|
+
entries: state.drillHistory,
|
|
712
|
+
coveredCategories: Object.fromEntries(state.drillCoveredCategories),
|
|
713
|
+
coveredScopes: Object.fromEntries(state.drillCoveredScopes),
|
|
714
|
+
}, drillConf?.historyCap ?? 100);
|
|
715
|
+
}
|
|
716
|
+
// ── Pre-verification ────────────────────────────────────────────────────────
|
|
717
|
+
/**
|
|
718
|
+
* Check if the current trajectory step's verification commands already pass.
|
|
719
|
+
* If so, mark the step complete and advance — avoids wasting an LLM cycle.
|
|
720
|
+
* Returns true if the step was advanced (caller should re-enter loop).
|
|
721
|
+
*/
|
|
722
|
+
export function tryPreVerifyTrajectoryStep(state) {
|
|
723
|
+
if (!state.activeTrajectory || !state.activeTrajectoryState || !state.currentTrajectoryStep)
|
|
724
|
+
return false;
|
|
725
|
+
const step = state.currentTrajectoryStep;
|
|
726
|
+
if (step.verification_commands.length === 0)
|
|
727
|
+
return false;
|
|
728
|
+
for (const cmd of step.verification_commands) {
|
|
729
|
+
const result = spawnSync('sh', ['-c', cmd], {
|
|
730
|
+
cwd: state.repoRoot,
|
|
731
|
+
timeout: 30000,
|
|
732
|
+
encoding: 'utf-8',
|
|
733
|
+
});
|
|
734
|
+
if (result.error || result.status !== 0)
|
|
735
|
+
return false; // not yet passing (or timeout)
|
|
736
|
+
}
|
|
737
|
+
// Optional measure gate
|
|
738
|
+
if (step.measure) {
|
|
739
|
+
const { value } = runMeasurement(step.measure.cmd, state.repoRoot);
|
|
740
|
+
if (value === null)
|
|
741
|
+
return false;
|
|
742
|
+
const met = step.measure.direction === 'up' ? value >= step.measure.target : value <= step.measure.target;
|
|
743
|
+
if (!met)
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
// All verification passes — mark complete and advance
|
|
747
|
+
const stepState = state.activeTrajectoryState.stepStates[step.id];
|
|
748
|
+
if (!stepState)
|
|
749
|
+
return false;
|
|
750
|
+
stepState.status = 'completed';
|
|
751
|
+
stepState.completedAt = Date.now();
|
|
752
|
+
console.log(chalk.green(` Trajectory step "${step.title}" already passing — advancing`));
|
|
753
|
+
const next = getTrajectoryNextStep(state.activeTrajectory, state.activeTrajectoryState.stepStates);
|
|
754
|
+
state.currentTrajectoryStep = next;
|
|
755
|
+
if (next) {
|
|
756
|
+
state.activeTrajectoryState.currentStepId = next.id;
|
|
757
|
+
if (state.activeTrajectoryState.stepStates[next.id]) {
|
|
758
|
+
state.activeTrajectoryState.stepStates[next.id].status = 'active';
|
|
759
|
+
}
|
|
760
|
+
console.log(chalk.cyan(` -> Next step: ${next.title}`));
|
|
761
|
+
}
|
|
762
|
+
else if (trajectoryComplete(state.activeTrajectory, state.activeTrajectoryState.stepStates)) {
|
|
763
|
+
console.log(chalk.green(` Trajectory "${state.activeTrajectory.name}" complete!`));
|
|
764
|
+
// Don't clear here — let the post-cycle handler finish the drill trajectory properly
|
|
765
|
+
}
|
|
766
|
+
saveTrajectoryState(state.repoRoot, state.activeTrajectoryState);
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
// ── Stratified sampling ──────────────────────────────────────────────────
|
|
770
|
+
/**
|
|
771
|
+
* Stratified proposal sampling — ensures category diversity while keeping quality high.
|
|
772
|
+
* Takes the best proposal from each category in round-robin, then fills remaining
|
|
773
|
+
* slots with the highest-scoring proposals from any category.
|
|
774
|
+
*/
|
|
775
|
+
function stratifiedSample(proposals, maxCount) {
|
|
776
|
+
if (proposals.length <= maxCount)
|
|
777
|
+
return proposals;
|
|
778
|
+
// Group by category
|
|
779
|
+
const byCategory = new Map();
|
|
780
|
+
for (const p of proposals) {
|
|
781
|
+
const cat = p.category || 'other';
|
|
782
|
+
const list = byCategory.get(cat) ?? [];
|
|
783
|
+
list.push(p);
|
|
784
|
+
byCategory.set(cat, list);
|
|
785
|
+
}
|
|
786
|
+
const selected = new Set();
|
|
787
|
+
const result = [];
|
|
788
|
+
// Round 1: take best from each category (ensures diversity)
|
|
789
|
+
for (const [, catProposals] of byCategory) {
|
|
790
|
+
if (result.length >= maxCount)
|
|
791
|
+
break;
|
|
792
|
+
if (catProposals.length === 0)
|
|
793
|
+
continue;
|
|
794
|
+
const idx = proposals.indexOf(catProposals[0]);
|
|
795
|
+
if (idx >= 0 && !selected.has(idx)) {
|
|
796
|
+
selected.add(idx);
|
|
797
|
+
result.push(catProposals[0]);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// Round 2: fill remaining slots with highest-scoring unselected proposals
|
|
801
|
+
for (let i = 0; i < proposals.length && result.length < maxCount; i++) {
|
|
802
|
+
if (!selected.has(i)) {
|
|
803
|
+
selected.add(i);
|
|
804
|
+
result.push(proposals[i]);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return result;
|
|
808
|
+
}
|
|
809
|
+
// ── Directive handling ────────────────────────────────────────────────────────
|
|
810
|
+
/**
|
|
811
|
+
* Apply pending drill directives from the hints system.
|
|
812
|
+
* Reads unconsumed directive hints, applies them to session state,
|
|
813
|
+
* and marks them as consumed.
|
|
814
|
+
*/
|
|
815
|
+
export function applyDrillDirectives(state) {
|
|
816
|
+
const hints = readHintsForDrill(state.repoRoot);
|
|
817
|
+
const directives = hints.filter(h => !h.consumed && h.directive);
|
|
818
|
+
if (directives.length === 0)
|
|
819
|
+
return;
|
|
820
|
+
for (const h of directives) {
|
|
821
|
+
switch (h.directive) {
|
|
822
|
+
case 'drill:pause':
|
|
823
|
+
state.drillMode = false;
|
|
824
|
+
console.log(chalk.cyan(' Drill: paused via nudge'));
|
|
825
|
+
state.displayAdapter.drillStateChanged(null);
|
|
826
|
+
break;
|
|
827
|
+
case 'drill:resume':
|
|
828
|
+
state.drillMode = true;
|
|
829
|
+
console.log(chalk.cyan(' Drill: resumed via nudge'));
|
|
830
|
+
state.displayAdapter.drillStateChanged({ active: true });
|
|
831
|
+
break;
|
|
832
|
+
case 'drill:disable':
|
|
833
|
+
state.drillMode = false;
|
|
834
|
+
console.log(chalk.cyan(' Drill: disabled via nudge'));
|
|
835
|
+
state.displayAdapter.drillStateChanged(null);
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
h.consumed = true;
|
|
839
|
+
}
|
|
840
|
+
// Write back consumed state
|
|
841
|
+
writeHintsForDrill(state.repoRoot, hints);
|
|
842
|
+
}
|
|
843
|
+
// ── Core drill logic ─────────────────────────────────────────────────────────
|
|
844
|
+
/**
|
|
845
|
+
* Check whether drill should generate a new trajectory, and if so, do it.
|
|
846
|
+
*
|
|
847
|
+
* Returns a result code so the caller knows whether to restart the loop
|
|
848
|
+
* (on 'generated') or fall through to a normal cycle.
|
|
849
|
+
*/
|
|
850
|
+
export async function maybeDrillGenerateTrajectory(state) {
|
|
851
|
+
// Adaptive cooldown — scales based on last trajectory outcome
|
|
852
|
+
const cooldownCycles = getDrillCooldown(state);
|
|
853
|
+
const cyclesSinceLastGen = state.cycleCount - state.drillLastGeneratedAtCycle;
|
|
854
|
+
if (state.drillTrajectoriesGenerated > 0 && cyclesSinceLastGen < cooldownCycles) {
|
|
855
|
+
console.log(chalk.gray(` Drill: cooldown (${cyclesSinceLastGen}/${cooldownCycles} cycles)`));
|
|
856
|
+
return 'cooldown';
|
|
857
|
+
}
|
|
858
|
+
// Gradient staleness check — skip survey when no external changes, reduce confidence boost when few changes
|
|
859
|
+
if (state.drillTrajectoriesGenerated > 0 && state.drillLastSurveyTimestamp) {
|
|
860
|
+
const stalenessLogBase = state.autoConf.drill?.stalenessLogBase ?? 11;
|
|
861
|
+
const staleness = measureCodeStaleness(state.repoRoot, state.drillLastSurveyTimestamp, stalenessLogBase);
|
|
862
|
+
if (staleness === 0) {
|
|
863
|
+
if (state.options.verbose) {
|
|
864
|
+
console.log(chalk.gray(' Drill: no code changes since last survey — skipping'));
|
|
865
|
+
}
|
|
866
|
+
return 'cooldown';
|
|
867
|
+
}
|
|
868
|
+
// Low staleness (few changes) — still survey but with less confidence discount
|
|
869
|
+
// This prevents full re-survey on trivial changes while allowing exploration on significant ones
|
|
870
|
+
if (staleness < 0.4 && state.options.verbose) {
|
|
871
|
+
console.log(chalk.gray(` Drill: minor changes since last survey (staleness: ${(staleness * 100).toFixed(0)}%)`));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Run broad survey — temporarily lower confidence threshold for wider discovery
|
|
875
|
+
console.log(chalk.cyan(`Drill: surveying codebase for trajectory generation...`));
|
|
876
|
+
state.drillLastSurveyTimestamp = Date.now();
|
|
877
|
+
const savedConfidence = state.effectiveMinConfidence;
|
|
878
|
+
const confidenceDiscount = state.autoConf.drill?.confidenceDiscount ?? 15;
|
|
879
|
+
state.effectiveMinConfidence = Math.max(0, state.effectiveMinConfidence - confidenceDiscount);
|
|
880
|
+
const allProposals = await runDrillSurvey(state);
|
|
881
|
+
state.effectiveMinConfidence = savedConfidence;
|
|
882
|
+
// Adaptive thresholds — scale with historical success rate
|
|
883
|
+
const { min: minProposals, max: maxProposals } = getAdaptiveProposalThresholds(state);
|
|
884
|
+
if (allProposals.length < minProposals) {
|
|
885
|
+
console.log(chalk.gray(` Drill: ${allProposals.length} proposal(s) found — below threshold (${minProposals}), running normal cycle`));
|
|
886
|
+
return 'insufficient'; // genuinely no proposals — codebase may be polished
|
|
887
|
+
}
|
|
888
|
+
// Freshness filter — drop proposals whose primary files have been modified since survey
|
|
889
|
+
// (they may no longer apply). Batches all files into a single git diff call.
|
|
890
|
+
let changedFiles;
|
|
891
|
+
try {
|
|
892
|
+
const allFiles = [...new Set(allProposals.flatMap(p => p.files.slice(0, 3)))];
|
|
893
|
+
if (allFiles.length > 0) {
|
|
894
|
+
const result = spawnSync('git', ['diff', '--name-only', '--', ...allFiles], {
|
|
895
|
+
cwd: state.repoRoot,
|
|
896
|
+
encoding: 'utf-8',
|
|
897
|
+
timeout: 5000,
|
|
898
|
+
});
|
|
899
|
+
changedFiles = (!result.error && result.status === 0 && result.stdout.trim())
|
|
900
|
+
? new Set(result.stdout.trim().split('\n').filter(Boolean))
|
|
901
|
+
: new Set();
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
changedFiles = new Set();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
changedFiles = new Set(); // on error, keep all proposals
|
|
909
|
+
}
|
|
910
|
+
const freshProposals = allProposals.filter(p => {
|
|
911
|
+
if (p.files.length === 0)
|
|
912
|
+
return true;
|
|
913
|
+
return !p.files.slice(0, 3).some(f => changedFiles.has(f));
|
|
914
|
+
});
|
|
915
|
+
if (freshProposals.length < allProposals.length) {
|
|
916
|
+
console.log(chalk.gray(` Drill: filtered ${allProposals.length - freshProposals.length} stale proposal(s) (modified since survey)`));
|
|
917
|
+
}
|
|
918
|
+
// Staleness check — if freshness filter dropped enough proposals to fall below threshold, return 'stale'
|
|
919
|
+
const { min: freshMin } = getAdaptiveProposalThresholds(state);
|
|
920
|
+
if (freshProposals.length < freshMin && freshProposals.length < allProposals.length) {
|
|
921
|
+
// Had proposals but freshness filter killed them — wait for external changes
|
|
922
|
+
console.log(chalk.gray(` Drill: ${allProposals.length - freshProposals.length} proposal(s) filtered as stale — waiting for external changes`));
|
|
923
|
+
return 'stale';
|
|
924
|
+
}
|
|
925
|
+
// Use fresh proposals for remaining checks (fall back to all if all filtered)
|
|
926
|
+
const proposalsForQuality = freshProposals.length >= freshMin
|
|
927
|
+
? freshProposals : allProposals;
|
|
928
|
+
// Quality gate — graduated: hard floor skips entirely, soft threshold adds guidance
|
|
929
|
+
const avgConfidence = proposalsForQuality.reduce((sum, p) => sum + (p.confidence ?? 50), 0) / proposalsForQuality.length;
|
|
930
|
+
const avgImpact = proposalsForQuality.reduce((sum, p) => sum + (p.impact_score ?? 5), 0) / proposalsForQuality.length;
|
|
931
|
+
const MIN_AVG_CONFIDENCE = state.autoConf.drill?.minAvgConfidence ?? 30;
|
|
932
|
+
const MIN_AVG_IMPACT = state.autoConf.drill?.minAvgImpact ?? 3;
|
|
933
|
+
// Hard floor: proposals are truly unusable — skip generation entirely
|
|
934
|
+
const HARD_FLOOR_CONFIDENCE = Math.max(10, Math.round(MIN_AVG_CONFIDENCE / 2));
|
|
935
|
+
const HARD_FLOOR_IMPACT = Math.max(1, Math.round(MIN_AVG_IMPACT / 2));
|
|
936
|
+
let qualityWarning;
|
|
937
|
+
if (avgConfidence < HARD_FLOOR_CONFIDENCE || avgImpact < HARD_FLOOR_IMPACT) {
|
|
938
|
+
console.log(chalk.gray(` Drill: proposals too weak (avg confidence: ${avgConfidence.toFixed(0)}, avg impact: ${avgImpact.toFixed(1)}) — skipping generation`));
|
|
939
|
+
return 'low_quality'; // proposals exist but quality is truly unusable
|
|
940
|
+
}
|
|
941
|
+
else if (avgConfidence < MIN_AVG_CONFIDENCE || avgImpact < MIN_AVG_IMPACT) {
|
|
942
|
+
// Soft threshold: proposals are weak but usable — proceed with conservative guidance
|
|
943
|
+
qualityWarning = `Proposal quality is below ideal (avg confidence: ${avgConfidence.toFixed(0)}/${MIN_AVG_CONFIDENCE}, avg impact: ${avgImpact.toFixed(1)}/${MIN_AVG_IMPACT}). Generate a SHORT, conservative trajectory (2-3 steps max) focused on the 1-2 highest-confidence proposals. Drop weaker proposals rather than spreading thin.`;
|
|
944
|
+
console.log(chalk.gray(` Drill: weak proposals (confidence: ${avgConfidence.toFixed(0)}, impact: ${avgImpact.toFixed(1)}) — generating conservative trajectory`));
|
|
945
|
+
}
|
|
946
|
+
// Freshness filter → cooldown bridge: store drop ratio for next cooldown calculation
|
|
947
|
+
const freshnessDropCount = allProposals.length - freshProposals.length;
|
|
948
|
+
state.drillLastFreshnessDropRatio = allProposals.length > 0
|
|
949
|
+
? freshnessDropCount / allProposals.length
|
|
950
|
+
: null;
|
|
951
|
+
// Capture generation telemetry — carried to outcome recording for empirical analysis
|
|
952
|
+
const proposalCategoryCount = new Set(proposalsForQuality.map(p => p.category || 'other')).size;
|
|
953
|
+
state.drillGenerationTelemetry = {
|
|
954
|
+
proposalAvgConfidence: avgConfidence,
|
|
955
|
+
proposalAvgImpact: avgImpact,
|
|
956
|
+
freshnessDropCount: freshnessDropCount > 0 ? freshnessDropCount : undefined,
|
|
957
|
+
proposalCategoryCount,
|
|
958
|
+
};
|
|
959
|
+
// Stratified sampling — ensure diversity across categories while prioritizing quality
|
|
960
|
+
const proposals = stratifiedSample(proposalsForQuality, maxProposals);
|
|
961
|
+
if (proposalsForQuality.length > maxProposals) {
|
|
962
|
+
console.log(chalk.gray(` Drill: stratified ${proposalsForQuality.length} proposals to ${proposals.length}`));
|
|
963
|
+
}
|
|
964
|
+
// Build context for trajectory generation
|
|
965
|
+
const historyBlock = formatDrillHistoryForPrompt(state);
|
|
966
|
+
const diversityHint = formatDiversityHint(state);
|
|
967
|
+
const sectorContext = formatSectorContextForPrompt(state);
|
|
968
|
+
// Integrate project learnings, taste profile, and dedup memory
|
|
969
|
+
const tasteContext = state.tasteProfile ? formatTasteForPrompt(state.tasteProfile) : undefined;
|
|
970
|
+
const learningsContext = state.autoConf.learningsEnabled
|
|
971
|
+
? formatLearningsForPrompt(selectRelevant(state.allLearnings, {}), state.autoConf.learningsBudget ?? 500) || undefined
|
|
972
|
+
: undefined;
|
|
973
|
+
const dedupContext = state.dedupMemory.length > 0
|
|
974
|
+
? formatDedupForPrompt(state.dedupMemory, 500) || undefined
|
|
975
|
+
: undefined;
|
|
976
|
+
// Build metrics hint from drill history
|
|
977
|
+
let metricsHint;
|
|
978
|
+
if (state.drillHistory.length >= 3) {
|
|
979
|
+
const metrics = computeDrillMetrics(state.drillHistory);
|
|
980
|
+
const parts = [];
|
|
981
|
+
parts.push(`Completion rate: ${(metrics.weightedCompletionRate * 100).toFixed(0)}% recent, ${(metrics.completionRate * 100).toFixed(0)}% overall (${metrics.totalTrajectories} trajectories)`);
|
|
982
|
+
parts.push(`Avg step completion: ${(metrics.weightedStepCompletionRate * 100).toFixed(0)}% recent, ${(metrics.avgStepCompletionRate * 100).toFixed(0)}% overall`);
|
|
983
|
+
if (metrics.topCategories.length > 0) {
|
|
984
|
+
parts.push(`High-success categories (prefer, can be ambitious): ${metrics.topCategories.join(', ')}`);
|
|
985
|
+
}
|
|
986
|
+
if (metrics.stalledCategories.length > 0) {
|
|
987
|
+
parts.push(`Frequently stalled categories (use conservative steps or avoid): ${metrics.stalledCategories.join(', ')}`);
|
|
988
|
+
}
|
|
989
|
+
// Per-category ambition hints — give the LLM granular guidance on which categories to push vs simplify
|
|
990
|
+
const catHints = [];
|
|
991
|
+
for (const [cat, stats] of Object.entries(metrics.categorySuccessRates)) {
|
|
992
|
+
if (stats.total < 2)
|
|
993
|
+
continue; // not enough data
|
|
994
|
+
if (stats.rate >= 0.7)
|
|
995
|
+
catHints.push(`${cat}: ambitious (${Math.round(stats.rate * 100)}% success)`);
|
|
996
|
+
else if (stats.rate < 0.3)
|
|
997
|
+
catHints.push(`${cat}: conservative (${Math.round(stats.rate * 100)}% success)`);
|
|
998
|
+
}
|
|
999
|
+
if (catHints.length > 0) {
|
|
1000
|
+
parts.push(`Per-category ambition: ${catHints.join(', ')}`);
|
|
1001
|
+
}
|
|
1002
|
+
if (metrics.step1FailureRate > 0.3) {
|
|
1003
|
+
parts.push(`WARNING: ${(metrics.step1FailureRate * 100).toFixed(0)}% of trajectories stall on step 1 — this is the #1 cause of wasted cycles. Step 1 MUST be a trivial "gimme" (1 file, zero dependencies, guaranteed success like adding a type annotation or fixing a lint error). Complex work belongs in step 2+.`);
|
|
1004
|
+
}
|
|
1005
|
+
else if (metrics.step1FailureRate > 0.15) {
|
|
1006
|
+
parts.push(`Step-1 failure rate: ${(metrics.step1FailureRate * 100).toFixed(0)}%. Keep first steps simple — 1-3 files, zero deps, one-cycle completable.`);
|
|
1007
|
+
}
|
|
1008
|
+
else if (metrics.step1FailureRate > 0) {
|
|
1009
|
+
parts.push(`Step-1 failure rate: ${(metrics.step1FailureRate * 100).toFixed(0)}% (healthy). First steps are landing well.`);
|
|
1010
|
+
}
|
|
1011
|
+
// Step-position failure analysis from telemetry — derive concrete step-count cap
|
|
1012
|
+
if (metrics.stepPositionFailureRates.length > 0) {
|
|
1013
|
+
const problematicPositions = metrics.stepPositionFailureRates.filter(p => p.failureRate > 0.5);
|
|
1014
|
+
if (problematicPositions.length > 0) {
|
|
1015
|
+
const posLabels = problematicPositions.map(p => `step ${p.position} (${Math.round(p.failureRate * 100)}% failure, n=${p.total})`);
|
|
1016
|
+
// Derive step-count cap: if position N fails >60%, cap at N-1 steps
|
|
1017
|
+
const highFailPositions = metrics.stepPositionFailureRates.filter(p => p.failureRate > 0.6 && p.total >= 3);
|
|
1018
|
+
if (highFailPositions.length > 0) {
|
|
1019
|
+
const firstHighFail = Math.min(...highFailPositions.map(p => p.position));
|
|
1020
|
+
const suggestedMax = Math.max(2, firstHighFail - 1);
|
|
1021
|
+
parts.push(`Step position risk: ${posLabels.join(', ')}. CONSTRAINT: limit trajectory to ${suggestedMax} steps maximum — positions ${firstHighFail}+ fail too often.`);
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
parts.push(`Step position risk: ${posLabels.join(', ')}. Keep these positions simpler or reduce trajectory length.`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// Scope-size → outcome correlation
|
|
1029
|
+
const entriesWithFiles = state.drillHistory.filter(h => h.modifiedFiles && h.modifiedFiles.length > 0);
|
|
1030
|
+
if (entriesWithFiles.length >= 3) {
|
|
1031
|
+
const largeScopeEntries = entriesWithFiles.filter(h => h.modifiedFiles.length > 10);
|
|
1032
|
+
const smallScopeEntries = entriesWithFiles.filter(h => h.modifiedFiles.length <= 10);
|
|
1033
|
+
if (largeScopeEntries.length >= 2 && smallScopeEntries.length >= 2) {
|
|
1034
|
+
const largeRate = largeScopeEntries.filter(h => h.outcome === 'completed').length / largeScopeEntries.length;
|
|
1035
|
+
const smallRate = smallScopeEntries.filter(h => h.outcome === 'completed').length / smallScopeEntries.length;
|
|
1036
|
+
if (largeRate < smallRate - 0.2) {
|
|
1037
|
+
parts.push(`Scope-size insight: trajectories touching >10 files succeed ${Math.round(largeRate * 100)}% vs ${Math.round(smallRate * 100)}% for ≤10 files. Prefer smaller, focused scopes.`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
metricsHint = parts.join('\n');
|
|
1042
|
+
}
|
|
1043
|
+
// Append quality warning to metrics hint (soft threshold guidance)
|
|
1044
|
+
if (qualityWarning) {
|
|
1045
|
+
metricsHint = metricsHint
|
|
1046
|
+
? `${metricsHint}\n\n${qualityWarning}`
|
|
1047
|
+
: qualityWarning;
|
|
1048
|
+
}
|
|
1049
|
+
// Build goal context for trajectory alignment
|
|
1050
|
+
let goalContext;
|
|
1051
|
+
if (state.activeGoal?.measure && state.activeGoalMeasurement) {
|
|
1052
|
+
const m = state.activeGoalMeasurement;
|
|
1053
|
+
const arrow = state.activeGoal.measure.direction === 'up' ? '>=' : '<=';
|
|
1054
|
+
goalContext = `Goal: "${state.activeGoal.name}" — current: ${m.current ?? 'unmeasured'}, target: ${arrow} ${state.activeGoal.measure.target} (gap: ${m.gapPercent}%)`;
|
|
1055
|
+
}
|
|
1056
|
+
// Extract dependency subgraph for proposal files
|
|
1057
|
+
let dependencyEdges;
|
|
1058
|
+
if (state.codebaseIndex?.dependency_edges) {
|
|
1059
|
+
const proposalModules = new Set();
|
|
1060
|
+
for (const p of proposals) {
|
|
1061
|
+
for (const f of p.files) {
|
|
1062
|
+
// Find the module that contains this file
|
|
1063
|
+
const dir = f.split('/').slice(0, -1).join('/');
|
|
1064
|
+
proposalModules.add(dir);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// Extract edges where either source or target is in proposal modules
|
|
1068
|
+
const edges = [];
|
|
1069
|
+
for (const [mod, deps] of Object.entries(state.codebaseIndex.dependency_edges)) {
|
|
1070
|
+
const relevantDeps = deps.filter(d => proposalModules.has(d) || proposalModules.has(mod));
|
|
1071
|
+
if (relevantDeps.length > 0 && (proposalModules.has(mod) || relevantDeps.some(d => proposalModules.has(d)))) {
|
|
1072
|
+
edges.push(`${mod} → ${relevantDeps.join(', ')}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (edges.length > 0 && edges.length <= 30) {
|
|
1076
|
+
dependencyEdges = edges.join('\n');
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
// Build causal context from recent trajectories — gives LLM a narrative arc
|
|
1080
|
+
let causalContext;
|
|
1081
|
+
if (state.drillHistory.length > 0) {
|
|
1082
|
+
const causalWindow = Math.max(1, Math.min(10, state.autoConf.drill?.causalWindow ?? 3));
|
|
1083
|
+
const windowEntries = state.drillHistory.slice(-causalWindow);
|
|
1084
|
+
const parts = [];
|
|
1085
|
+
for (let i = 0; i < windowEntries.length; i++) {
|
|
1086
|
+
const entry = windowEntries[i];
|
|
1087
|
+
const isNewest = i === windowEntries.length - 1;
|
|
1088
|
+
const ago = windowEntries.length - i;
|
|
1089
|
+
if (isNewest) {
|
|
1090
|
+
// Most recent: full detail with granular completion
|
|
1091
|
+
const pct = entry.completionPct !== undefined && entry.completionPct !== null
|
|
1092
|
+
? Math.round(entry.completionPct * 100)
|
|
1093
|
+
: Math.round((entry.stepsCompleted / Math.max(1, entry.stepsTotal)) * 100);
|
|
1094
|
+
parts.push(`Last trajectory: "${entry.description}" (${entry.outcome}, ${pct}% complete)`);
|
|
1095
|
+
if (entry.completedStepSummaries && entry.completedStepSummaries.length > 0) {
|
|
1096
|
+
parts.push(`What was done: ${entry.completedStepSummaries.join('; ')}`);
|
|
1097
|
+
}
|
|
1098
|
+
if (entry.modifiedFiles && entry.modifiedFiles.length > 0) {
|
|
1099
|
+
parts.push(`Files changed: ${entry.modifiedFiles.slice(0, 10).join(', ')}`);
|
|
1100
|
+
}
|
|
1101
|
+
if (entry.failedSteps && entry.failedSteps.length > 0) {
|
|
1102
|
+
const reasons = entry.failedSteps.map(f => `"${f.title}"${f.reason ? `: ${f.reason.slice(0, 80)}` : ''}`);
|
|
1103
|
+
parts.push(`What failed (may now be unblocked by survey proposals): ${reasons.join('; ')}`);
|
|
1104
|
+
}
|
|
1105
|
+
if (entry.outcome === 'completed') {
|
|
1106
|
+
parts.push('The previous trajectory SUCCEEDED — build on this momentum. Propose follow-up work like tests, documentation, or deeper refactors in the same area.');
|
|
1107
|
+
}
|
|
1108
|
+
else if (pct >= 70) {
|
|
1109
|
+
parts.push(`The previous trajectory was ${pct}% complete before stalling — nearly done. Consider a targeted follow-up trajectory to finish the remaining work.`);
|
|
1110
|
+
}
|
|
1111
|
+
else if (pct >= 30) {
|
|
1112
|
+
parts.push('The previous trajectory PARTIALLY completed — some work was done. Try a different angle for the remaining work, or simplify the approach.');
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
parts.push('The previous trajectory STALLED early — avoid the same approach entirely. Try a different angle or much simpler steps.');
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
// Older entries: summary only
|
|
1120
|
+
parts.push(`[${ago} ago] "${entry.description}" — ${entry.outcome}, categories: ${entry.categories.join(', ') || 'mixed'}`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
causalContext = parts.join('\n');
|
|
1124
|
+
}
|
|
1125
|
+
// Adaptive ambition — scale first-step complexity based on track record
|
|
1126
|
+
const baseAmbition = computeAmbitionLevel(state);
|
|
1127
|
+
// Proposal quality → ambition override: if current proposals are weak, downgrade;
|
|
1128
|
+
// if strong, upgrade. This closes the loop between proposal quality and generation complexity.
|
|
1129
|
+
let effectiveAmbition = baseAmbition;
|
|
1130
|
+
if (avgConfidence < 40 && effectiveAmbition !== 'conservative') {
|
|
1131
|
+
effectiveAmbition = effectiveAmbition === 'ambitious' ? 'moderate' : 'conservative';
|
|
1132
|
+
if (state.options.verbose)
|
|
1133
|
+
console.log(chalk.gray(` Ambition downgraded to ${effectiveAmbition} (low proposal confidence: ${avgConfidence.toFixed(0)})`));
|
|
1134
|
+
}
|
|
1135
|
+
else if (avgConfidence > 70 && effectiveAmbition !== 'ambitious' && state.drillHistory.length >= 5) {
|
|
1136
|
+
effectiveAmbition = effectiveAmbition === 'conservative' ? 'moderate' : 'ambitious';
|
|
1137
|
+
if (state.options.verbose)
|
|
1138
|
+
console.log(chalk.gray(` Ambition upgraded to ${effectiveAmbition} (high proposal confidence: ${avgConfidence.toFixed(0)})`));
|
|
1139
|
+
}
|
|
1140
|
+
// Multi-trajectory arc guidance — directional hints across consecutive trajectories
|
|
1141
|
+
// Derive goal category from active goal's formula categories for goal-aligned guidance
|
|
1142
|
+
const goalCategory = state.activeGoal?.categories?.[0];
|
|
1143
|
+
const arcGuidance = computeArcGuidance(state, goalCategory);
|
|
1144
|
+
// Merge arc guidance into causal context as a meta-narrative preamble
|
|
1145
|
+
// to avoid conflicting prompt sections and reduce token spend
|
|
1146
|
+
if (arcGuidance) {
|
|
1147
|
+
causalContext = causalContext
|
|
1148
|
+
? `[Campaign direction]\n${arcGuidance}\n\n[Recent trajectory detail]\n${causalContext}`
|
|
1149
|
+
: arcGuidance;
|
|
1150
|
+
}
|
|
1151
|
+
console.log(chalk.cyan(` Drill: generating trajectory from ${proposals.length} proposal(s)...`));
|
|
1152
|
+
if (effectiveAmbition !== 'moderate') {
|
|
1153
|
+
console.log(chalk.gray(` Ambition: ${effectiveAmbition}${effectiveAmbition !== baseAmbition ? ` (adjusted from ${baseAmbition})` : ''}`));
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
const result = await generateTrajectoryFromProposals({
|
|
1157
|
+
proposals: proposals.map(p => ({
|
|
1158
|
+
title: p.title,
|
|
1159
|
+
description: p.description,
|
|
1160
|
+
category: p.category,
|
|
1161
|
+
files: p.files,
|
|
1162
|
+
allowed_paths: p.allowed_paths,
|
|
1163
|
+
acceptance_criteria: p.acceptance_criteria,
|
|
1164
|
+
verification_commands: p.verification_commands,
|
|
1165
|
+
confidence: p.confidence,
|
|
1166
|
+
impact_score: p.impact_score,
|
|
1167
|
+
rationale: p.rationale,
|
|
1168
|
+
estimated_complexity: p.estimated_complexity,
|
|
1169
|
+
})),
|
|
1170
|
+
repoRoot: state.repoRoot,
|
|
1171
|
+
previousTrajectories: historyBlock || undefined,
|
|
1172
|
+
diversityHint: diversityHint || undefined,
|
|
1173
|
+
sectorContext: sectorContext || undefined,
|
|
1174
|
+
tasteContext,
|
|
1175
|
+
learningsContext,
|
|
1176
|
+
dedupContext,
|
|
1177
|
+
goalContext,
|
|
1178
|
+
metricsHint,
|
|
1179
|
+
dependencyEdges,
|
|
1180
|
+
causalContext,
|
|
1181
|
+
ambitionLevel: effectiveAmbition,
|
|
1182
|
+
});
|
|
1183
|
+
// Activate the generated trajectory
|
|
1184
|
+
const trajState = activateTrajectory(state.repoRoot, result.trajectory.name);
|
|
1185
|
+
if (!trajState) {
|
|
1186
|
+
console.log(chalk.yellow(' Drill: trajectory generated but activation failed'));
|
|
1187
|
+
return 'failed';
|
|
1188
|
+
}
|
|
1189
|
+
// Load into session state
|
|
1190
|
+
const traj = loadTrajectory(state.repoRoot, result.trajectory.name);
|
|
1191
|
+
if (!traj) {
|
|
1192
|
+
console.log(chalk.yellow(' Drill: trajectory generated but could not be loaded'));
|
|
1193
|
+
return 'failed';
|
|
1194
|
+
}
|
|
1195
|
+
state.activeTrajectory = traj;
|
|
1196
|
+
state.activeTrajectoryState = trajState;
|
|
1197
|
+
state.currentTrajectoryStep = getTrajectoryNextStep(traj, trajState.stepStates);
|
|
1198
|
+
// Update drill tracking
|
|
1199
|
+
state.drillLastGeneratedAtCycle = state.cycleCount;
|
|
1200
|
+
state.drillTrajectoriesGenerated++;
|
|
1201
|
+
const stepCount = traj.steps.length;
|
|
1202
|
+
console.log(chalk.green(` Drill: trajectory "${traj.name}" activated (${stepCount} steps)`));
|
|
1203
|
+
console.log(chalk.gray(` ${result.filePath}`));
|
|
1204
|
+
if (state.currentTrajectoryStep) {
|
|
1205
|
+
console.log(chalk.cyan(` First step: ${state.currentTrajectoryStep.title}`));
|
|
1206
|
+
}
|
|
1207
|
+
if (state.drillHistory.length > 0) {
|
|
1208
|
+
console.log(chalk.gray(` Session trajectories: ${state.drillHistory.length} previous`));
|
|
1209
|
+
}
|
|
1210
|
+
return 'generated';
|
|
1211
|
+
}
|
|
1212
|
+
catch (err) {
|
|
1213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1214
|
+
console.log(chalk.yellow(` Drill: trajectory generation failed — ${msg}`));
|
|
1215
|
+
// Don't update drillLastGeneratedAtCycle on failure — allow retry on next cycle
|
|
1216
|
+
// instead of forcing a full cooldown after a transient error
|
|
1217
|
+
return 'failed';
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Run a broad survey scout to gather proposals for trajectory generation.
|
|
1222
|
+
* Wraps scoutAllSectors() — reuses existing multi-sector scout logic.
|
|
1223
|
+
* Proposals are returned pre-ranked by score (highest first).
|
|
1224
|
+
*/
|
|
1225
|
+
export async function runDrillSurvey(state) {
|
|
1226
|
+
const { proposals } = await scoutAllSectors(state);
|
|
1227
|
+
return proposals;
|
|
1228
|
+
}
|
|
1229
|
+
//# sourceMappingURL=solo-auto-drill.js.map
|