@promptwheel/mcp 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -0
- package/dist/advance-helpers.d.ts +27 -0
- package/dist/advance-helpers.d.ts.map +1 -0
- package/dist/advance-helpers.js +127 -0
- package/dist/advance-helpers.js.map +1 -0
- package/dist/advance-prompts.d.ts +48 -0
- package/dist/advance-prompts.d.ts.map +1 -0
- package/dist/advance-prompts.js +420 -0
- package/dist/advance-prompts.js.map +1 -0
- package/dist/advance.d.ts +30 -0
- package/dist/advance.d.ts.map +1 -0
- package/dist/advance.js +752 -0
- package/dist/advance.js.map +1 -0
- package/dist/codebase-index.d.ts +5 -0
- package/dist/codebase-index.d.ts.map +1 -0
- package/dist/codebase-index.js +5 -0
- package/dist/codebase-index.js.map +1 -0
- package/dist/dedup-memory.d.ts +30 -0
- package/dist/dedup-memory.d.ts.map +1 -0
- package/dist/dedup-memory.js +76 -0
- package/dist/dedup-memory.js.map +1 -0
- package/dist/direct-client.d.ts +57 -0
- package/dist/direct-client.d.ts.map +1 -0
- package/dist/direct-client.js +92 -0
- package/dist/direct-client.js.map +1 -0
- package/dist/event-handlers-qa.d.ts +5 -0
- package/dist/event-handlers-qa.d.ts.map +1 -0
- package/dist/event-handlers-qa.js +186 -0
- package/dist/event-handlers-qa.js.map +1 -0
- package/dist/event-handlers-scout.d.ts +5 -0
- package/dist/event-handlers-scout.d.ts.map +1 -0
- package/dist/event-handlers-scout.js +270 -0
- package/dist/event-handlers-scout.js.map +1 -0
- package/dist/event-handlers-ticket.d.ts +5 -0
- package/dist/event-handlers-ticket.d.ts.map +1 -0
- package/dist/event-handlers-ticket.js +232 -0
- package/dist/event-handlers-ticket.js.map +1 -0
- package/dist/event-helpers.d.ts +46 -0
- package/dist/event-helpers.d.ts.map +1 -0
- package/dist/event-helpers.js +125 -0
- package/dist/event-helpers.js.map +1 -0
- package/dist/event-processor.d.ts +15 -0
- package/dist/event-processor.d.ts.map +1 -0
- package/dist/event-processor.js +111 -0
- package/dist/event-processor.js.map +1 -0
- package/dist/formulas.d.ts +27 -0
- package/dist/formulas.d.ts.map +1 -0
- package/dist/formulas.js +109 -0
- package/dist/formulas.js.map +1 -0
- package/dist/guidelines.d.ts +16 -0
- package/dist/guidelines.d.ts.map +1 -0
- package/dist/guidelines.js +44 -0
- package/dist/guidelines.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/learnings.d.ts +42 -0
- package/dist/learnings.d.ts.map +1 -0
- package/dist/learnings.js +117 -0
- package/dist/learnings.js.map +1 -0
- package/dist/project-metadata.d.ts +34 -0
- package/dist/project-metadata.d.ts.map +1 -0
- package/dist/project-metadata.js +617 -0
- package/dist/project-metadata.js.map +1 -0
- package/dist/proposals.d.ts +23 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/proposals.js +201 -0
- package/dist/proposals.js.map +1 -0
- package/dist/qa-stats.d.ts +51 -0
- package/dist/qa-stats.d.ts.map +1 -0
- package/dist/qa-stats.js +121 -0
- package/dist/qa-stats.js.map +1 -0
- package/dist/run-manager.d.ts +97 -0
- package/dist/run-manager.d.ts.map +1 -0
- package/dist/run-manager.js +583 -0
- package/dist/run-manager.js.map +1 -0
- package/dist/run-state-bridge.d.ts +13 -0
- package/dist/run-state-bridge.d.ts.map +1 -0
- package/dist/run-state-bridge.js +101 -0
- package/dist/run-state-bridge.js.map +1 -0
- package/dist/scope-policy.d.ts +83 -0
- package/dist/scope-policy.d.ts.map +1 -0
- package/dist/scope-policy.js +278 -0
- package/dist/scope-policy.js.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +35 -0
- package/dist/server.js.map +1 -0
- package/dist/spindle.d.ts +41 -0
- package/dist/spindle.d.ts.map +1 -0
- package/dist/spindle.js +230 -0
- package/dist/spindle.js.map +1 -0
- package/dist/state.d.ts +36 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +50 -0
- package/dist/state.js.map +1 -0
- package/dist/ticket-worker.d.ts +37 -0
- package/dist/ticket-worker.d.ts.map +1 -0
- package/dist/ticket-worker.js +527 -0
- package/dist/ticket-worker.js.map +1 -0
- package/dist/tool-registry.d.ts +35 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +129 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tools/execute.d.ts +17 -0
- package/dist/tools/execute.d.ts.map +1 -0
- package/dist/tools/execute.js +418 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/git.d.ts +7 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/git.js +98 -0
- package/dist/tools/git.js.map +1 -0
- package/dist/tools/intelligence.d.ts +10 -0
- package/dist/tools/intelligence.d.ts.map +1 -0
- package/dist/tools/intelligence.js +432 -0
- package/dist/tools/intelligence.js.map +1 -0
- package/dist/tools/session.d.ts +7 -0
- package/dist/tools/session.d.ts.map +1 -0
- package/dist/tools/session.js +533 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/trajectory.d.ts +10 -0
- package/dist/tools/trajectory.d.ts.map +1 -0
- package/dist/tools/trajectory.js +374 -0
- package/dist/tools/trajectory.js.map +1 -0
- package/dist/trajectory-io.d.ts +21 -0
- package/dist/trajectory-io.d.ts.map +1 -0
- package/dist/trajectory-io.js +105 -0
- package/dist/trajectory-io.js.map +1 -0
- package/dist/types.d.ts +229 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
package/dist/advance.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advance Engine — the deterministic state machine.
|
|
3
|
+
*
|
|
4
|
+
* `advance()` is called by the client on every loop iteration.
|
|
5
|
+
* It returns what to do next (prompt + constraints) or STOP.
|
|
6
|
+
*
|
|
7
|
+
* State machine transitions (from docs/PLUGIN_ROADMAP.md):
|
|
8
|
+
*
|
|
9
|
+
* SCOUT → NEXT_TICKET | DONE | FAILED_BUDGET
|
|
10
|
+
* NEXT_TICKET → PLAN | SCOUT | DONE
|
|
11
|
+
* PLAN → EXECUTE | PLAN | BLOCKED_NEEDS_HUMAN | FAILED_BUDGET
|
|
12
|
+
* EXECUTE → QA | NEXT_TICKET | BLOCKED_NEEDS_HUMAN | FAILED_BUDGET | FAILED_SPINDLE
|
|
13
|
+
* QA → PR | EXECUTE | NEXT_TICKET | FAILED_BUDGET
|
|
14
|
+
* PR → NEXT_TICKET | FAILED_VALIDATION
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { repos, EXECUTION_DEFAULTS } from '@promptwheel/core';
|
|
19
|
+
import { TERMINAL_PHASES } from './types.js';
|
|
20
|
+
import { deriveScopePolicy } from './scope-policy.js';
|
|
21
|
+
import { checkSpindle, getFileEditWarnings } from './spindle.js';
|
|
22
|
+
import { loadFormula } from './formulas.js';
|
|
23
|
+
import { loadGuidelines, formatGuidelinesForPrompt } from './guidelines.js';
|
|
24
|
+
import { detectProjectMetadata, formatMetadataForPrompt } from './project-metadata.js';
|
|
25
|
+
import { formatIndexForPrompt, refreshCodebaseIndex, hasStructuralChanges } from './codebase-index.js';
|
|
26
|
+
import { buildProposalReviewPrompt } from './proposals.js';
|
|
27
|
+
import { loadDedupMemory, formatDedupForPrompt } from './dedup-memory.js';
|
|
28
|
+
import { computeRetryRisk, scoreStrategies, buildCriticBlock, buildPlanRejectionCriticBlock, } from '@promptwheel/core/critic/shared';
|
|
29
|
+
import { pickNextSector as pickNextSectorCore, computeCoverage as computeCoverageCore, buildSectorSummary as buildSectorSummaryCore, } from '@promptwheel/core/sectors/shared';
|
|
30
|
+
import { getNextStep as getTrajectoryNextStep, formatTrajectoryForPrompt, } from '@promptwheel/core/trajectory/shared';
|
|
31
|
+
import { buildScoutPrompt, buildScoutEscalation, buildPlanPrompt, buildExecutePrompt, buildQaPrompt, buildPrPrompt, buildInlineTicketPrompt, } from './advance-prompts.js';
|
|
32
|
+
import { loadTrajectoryData, loadSectorsState, buildLearningsBlock, buildRiskContextBlock, getScoutAutoApprove, getExecuteAutoApprove, getQaAutoApprove, getPrAutoApprove, } from './advance-helpers.js';
|
|
33
|
+
const MAX_PLAN_REJECTIONS = 3;
|
|
34
|
+
const MAX_QA_RETRIES = EXECUTION_DEFAULTS.MAX_QA_RETRIES;
|
|
35
|
+
const MAX_SPINDLE_RECOVERIES = 3;
|
|
36
|
+
/**
|
|
37
|
+
* Core advance function. Called once per loop iteration.
|
|
38
|
+
* Returns the next action for the client to perform.
|
|
39
|
+
*/
|
|
40
|
+
export async function advance(ctx) {
|
|
41
|
+
const { run, db } = ctx;
|
|
42
|
+
const s = run.require();
|
|
43
|
+
// Increment step
|
|
44
|
+
run.incrementStep();
|
|
45
|
+
run.appendEvent('ADVANCE_CALLED', { phase: s.phase, step: s.step_count });
|
|
46
|
+
// -----------------------------------------------------------------------
|
|
47
|
+
// Budget checks (run before any phase logic)
|
|
48
|
+
// -----------------------------------------------------------------------
|
|
49
|
+
const budgetResult = checkBudgets(run);
|
|
50
|
+
if (budgetResult) {
|
|
51
|
+
return budgetResult;
|
|
52
|
+
}
|
|
53
|
+
// -----------------------------------------------------------------------
|
|
54
|
+
// Fire budget warnings at 80%
|
|
55
|
+
// -----------------------------------------------------------------------
|
|
56
|
+
const warnings = run.getBudgetWarnings();
|
|
57
|
+
if (warnings.length > 0) {
|
|
58
|
+
run.appendEvent('BUDGET_WARNING', { warnings });
|
|
59
|
+
}
|
|
60
|
+
// -----------------------------------------------------------------------
|
|
61
|
+
// Spindle check (only in EXECUTE/QA — active work phases)
|
|
62
|
+
// -----------------------------------------------------------------------
|
|
63
|
+
if (s.phase === 'EXECUTE' || s.phase === 'QA') {
|
|
64
|
+
const spindleResult = checkSpindle(s.spindle);
|
|
65
|
+
// File edit frequency warnings
|
|
66
|
+
const fileWarnings = getFileEditWarnings(s.spindle);
|
|
67
|
+
if (fileWarnings.length > 0) {
|
|
68
|
+
run.appendEvent('SPINDLE_WARNING', { file_edit_warnings: fileWarnings });
|
|
69
|
+
}
|
|
70
|
+
if (spindleResult.shouldAbort) {
|
|
71
|
+
run.appendEvent('SPINDLE_ABORT', {
|
|
72
|
+
reason: spindleResult.reason,
|
|
73
|
+
confidence: spindleResult.confidence,
|
|
74
|
+
diagnostics: spindleResult.diagnostics,
|
|
75
|
+
});
|
|
76
|
+
if (s.current_ticket_id) {
|
|
77
|
+
await repos.tickets.updateStatus(db, s.current_ticket_id, 'blocked');
|
|
78
|
+
run.failTicket(`Spindle abort: ${spindleResult.reason}`);
|
|
79
|
+
}
|
|
80
|
+
s.spindle_recoveries++;
|
|
81
|
+
if (s.spindle_recoveries >= MAX_SPINDLE_RECOVERIES) {
|
|
82
|
+
run.setPhase('FAILED_SPINDLE');
|
|
83
|
+
return stopResponse(run, 'FAILED_SPINDLE', `Spindle loop detected: ${spindleResult.reason} (confidence: ${(spindleResult.confidence * 100).toFixed(0)}%) — recovery cap reached (${s.spindle_recoveries}/${MAX_SPINDLE_RECOVERIES})`);
|
|
84
|
+
}
|
|
85
|
+
run.resetForSpindleRecovery();
|
|
86
|
+
run.setPhase('NEXT_TICKET');
|
|
87
|
+
return advance(ctx);
|
|
88
|
+
}
|
|
89
|
+
if (spindleResult.shouldBlock) {
|
|
90
|
+
run.appendEvent('SPINDLE_WARNING', {
|
|
91
|
+
reason: spindleResult.reason,
|
|
92
|
+
diagnostics: spindleResult.diagnostics,
|
|
93
|
+
action: 'BLOCKED_NEEDS_HUMAN',
|
|
94
|
+
});
|
|
95
|
+
if (s.current_ticket_id) {
|
|
96
|
+
await repos.tickets.updateStatus(db, s.current_ticket_id, 'blocked');
|
|
97
|
+
run.failTicket(`Spindle block: ${spindleResult.reason}`);
|
|
98
|
+
}
|
|
99
|
+
s.spindle_recoveries++;
|
|
100
|
+
if (s.spindle_recoveries >= MAX_SPINDLE_RECOVERIES) {
|
|
101
|
+
run.setPhase('BLOCKED_NEEDS_HUMAN');
|
|
102
|
+
return stopResponse(run, 'BLOCKED_NEEDS_HUMAN', `Spindle: ${spindleResult.reason} — recovery cap reached (${s.spindle_recoveries}/${MAX_SPINDLE_RECOVERIES}). Needs human intervention.`);
|
|
103
|
+
}
|
|
104
|
+
run.resetForSpindleRecovery();
|
|
105
|
+
run.setPhase('NEXT_TICKET');
|
|
106
|
+
return advance(ctx);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// -----------------------------------------------------------------------
|
|
110
|
+
// Terminal state check
|
|
111
|
+
// -----------------------------------------------------------------------
|
|
112
|
+
if (TERMINAL_PHASES.has(s.phase)) {
|
|
113
|
+
return stopResponse(run, s.phase, terminalReason(s.phase));
|
|
114
|
+
}
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
// Phase dispatch
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
switch (s.phase) {
|
|
119
|
+
case 'SCOUT':
|
|
120
|
+
return advanceScout(ctx);
|
|
121
|
+
case 'NEXT_TICKET':
|
|
122
|
+
return advanceNextTicket(ctx);
|
|
123
|
+
case 'PLAN':
|
|
124
|
+
return advancePlan(ctx);
|
|
125
|
+
case 'EXECUTE':
|
|
126
|
+
return advanceExecute(ctx);
|
|
127
|
+
case 'QA':
|
|
128
|
+
return advanceQa(ctx);
|
|
129
|
+
case 'PR':
|
|
130
|
+
return advancePr(ctx);
|
|
131
|
+
case 'PARALLEL_EXECUTE':
|
|
132
|
+
return advanceParallelExecute(ctx);
|
|
133
|
+
default:
|
|
134
|
+
return stopResponse(run, 'FAILED_VALIDATION', `Unknown phase: ${s.phase}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Phase handlers
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
async function advanceScout(ctx) {
|
|
141
|
+
const { run, db } = ctx;
|
|
142
|
+
const s = run.require();
|
|
143
|
+
// If skip_review is on and stale pending_proposals exist, clear them
|
|
144
|
+
if (s.skip_review && s.pending_proposals !== null) {
|
|
145
|
+
s.pending_proposals = null;
|
|
146
|
+
}
|
|
147
|
+
// If pending proposals exist, return adversarial review prompt
|
|
148
|
+
if (!s.skip_review && s.pending_proposals !== null) {
|
|
149
|
+
// Convert raw proposals to ValidatedProposal shape for the review prompt
|
|
150
|
+
const forReview = s.pending_proposals.map(p => ({
|
|
151
|
+
category: p.category ?? 'unknown',
|
|
152
|
+
title: p.title ?? 'Untitled',
|
|
153
|
+
description: p.description ?? '',
|
|
154
|
+
acceptance_criteria: p.acceptance_criteria ?? [],
|
|
155
|
+
verification_commands: p.verification_commands ?? [],
|
|
156
|
+
allowed_paths: p.allowed_paths ?? [],
|
|
157
|
+
files: p.files ?? [],
|
|
158
|
+
confidence: p.confidence ?? 0,
|
|
159
|
+
impact_score: p.impact_score ?? 5,
|
|
160
|
+
rationale: p.rationale ?? '',
|
|
161
|
+
estimated_complexity: p.estimated_complexity ?? 'moderate',
|
|
162
|
+
risk: p.risk ?? 'medium',
|
|
163
|
+
touched_files_estimate: p.touched_files_estimate ?? 0,
|
|
164
|
+
rollback_note: p.rollback_note ?? '',
|
|
165
|
+
}));
|
|
166
|
+
const reviewPrompt = buildProposalReviewPrompt(forReview);
|
|
167
|
+
return promptResponse(run, 'SCOUT', reviewPrompt, 'Reviewing proposals adversarially', {
|
|
168
|
+
allowed_paths: [s.scope],
|
|
169
|
+
denied_paths: [],
|
|
170
|
+
denied_patterns: [],
|
|
171
|
+
max_files: 0,
|
|
172
|
+
max_lines: 0,
|
|
173
|
+
required_commands: [],
|
|
174
|
+
plan_required: false,
|
|
175
|
+
auto_approve_patterns: getScoutAutoApprove(),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Check if we already have ready tickets in backlog
|
|
179
|
+
const readyTickets = await repos.tickets.listByProject(db, s.project_id, { status: 'ready', limit: 1 });
|
|
180
|
+
if (readyTickets.length > 0) {
|
|
181
|
+
// Tickets exist, move to assignment
|
|
182
|
+
run.setPhase('NEXT_TICKET');
|
|
183
|
+
return advance(ctx);
|
|
184
|
+
}
|
|
185
|
+
// No ready tickets — return scout prompt for client to execute
|
|
186
|
+
const recentTickets = await repos.tickets.getRecentlyCompleted(db, s.project_id, 20);
|
|
187
|
+
const dedupContext = recentTickets.map(t => t.title);
|
|
188
|
+
// Load dedup memory (with decay) — weighted, persistent awareness of completed work
|
|
189
|
+
const dedupMemory = loadDedupMemory(ctx.project.rootPath);
|
|
190
|
+
const dedupMemoryBlock = formatDedupForPrompt(dedupMemory);
|
|
191
|
+
const dedupBlock = dedupMemoryBlock ? dedupMemoryBlock + '\n\n' : '';
|
|
192
|
+
const hints = run.consumeHints();
|
|
193
|
+
// Load formula if specified
|
|
194
|
+
const formula = s.formula ? loadFormula(s.formula, ctx.project.rootPath) : null;
|
|
195
|
+
const guidelines = loadGuidelines(ctx.project.rootPath);
|
|
196
|
+
const guidelinesBlock = guidelines ? formatGuidelinesForPrompt(guidelines) + '\n\n' : '';
|
|
197
|
+
// Detect project metadata for tooling context
|
|
198
|
+
const projectMeta = detectProjectMetadata(ctx.project.rootPath);
|
|
199
|
+
const metadataBlock = formatMetadataForPrompt(projectMeta) + '\n\n';
|
|
200
|
+
// Refresh codebase index if structure changed (dirty flag from tickets, or external mtime check)
|
|
201
|
+
if (s.codebase_index) {
|
|
202
|
+
const needsRefresh = s.codebase_index_dirty
|
|
203
|
+
|| hasStructuralChanges(s.codebase_index, ctx.project.rootPath);
|
|
204
|
+
if (needsRefresh) {
|
|
205
|
+
s.codebase_index = refreshCodebaseIndex(s.codebase_index, ctx.project.rootPath, s.scout_exclude_dirs);
|
|
206
|
+
s.codebase_index_dirty = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Sector rotation: use core pickNextSector for full 9-tiebreaker sort
|
|
210
|
+
// Skip when a trajectory is active — trajectory scope takes priority
|
|
211
|
+
if (!s.active_trajectory) {
|
|
212
|
+
try {
|
|
213
|
+
const sectorsState = loadSectorsState(ctx.project.rootPath);
|
|
214
|
+
if (sectorsState) {
|
|
215
|
+
const picked = pickNextSectorCore(sectorsState, s.scout_cycles);
|
|
216
|
+
if (picked) {
|
|
217
|
+
s.scope = picked.scope;
|
|
218
|
+
s.selected_sector_path = picked.sector.path;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
run.appendEvent('SECTOR_ROTATION_FAILED', { error: err instanceof Error ? err.message : String(err) });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Build codebase index block — use cycles + retries so retries advance the chunk
|
|
227
|
+
const chunkOffset = s.scout_cycles + s.scout_retries;
|
|
228
|
+
const indexBlock = s.codebase_index ? formatIndexForPrompt(s.codebase_index, chunkOffset) + '\n\n' : '';
|
|
229
|
+
// Build escalation block if retrying after 0 proposals
|
|
230
|
+
const escalationBlock = s.scout_retries > 0
|
|
231
|
+
? buildScoutEscalation(s.scout_retries, s.scout_exploration_log, s.scouted_dirs, s.codebase_index) + '\n\n'
|
|
232
|
+
: '';
|
|
233
|
+
const learningsBlock = buildLearningsBlock(run, [s.scope, ...s.scouted_dirs], []);
|
|
234
|
+
const cov = run.buildDigest().coverage;
|
|
235
|
+
let sectorSummary;
|
|
236
|
+
let sectorPercent;
|
|
237
|
+
try {
|
|
238
|
+
const sectorsState = loadSectorsState(ctx.project.rootPath);
|
|
239
|
+
if (sectorsState) {
|
|
240
|
+
const metrics = computeCoverageCore(sectorsState);
|
|
241
|
+
sectorPercent = metrics.sectorPercent;
|
|
242
|
+
sectorSummary = buildSectorSummaryCore(sectorsState, s.selected_sector_path ?? '');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.warn(`[promptwheel] Sector summary failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
247
|
+
}
|
|
248
|
+
const coverageCtx = cov.sectors_total > 0
|
|
249
|
+
? { scannedSectors: cov.sectors_scanned, totalSectors: cov.sectors_total, percent: cov.percent, sectorPercent, sectorSummary }
|
|
250
|
+
: undefined;
|
|
251
|
+
// Trajectory context
|
|
252
|
+
let trajectoryBlock = '';
|
|
253
|
+
try {
|
|
254
|
+
const trajData = loadTrajectoryData(ctx.project.rootPath);
|
|
255
|
+
if (trajData) {
|
|
256
|
+
const currentStep = getTrajectoryNextStep(trajData.trajectory, trajData.state.stepStates);
|
|
257
|
+
if (currentStep) {
|
|
258
|
+
trajectoryBlock = formatTrajectoryForPrompt(trajData.trajectory, trajData.state.stepStates, currentStep) + '\n\n';
|
|
259
|
+
s.active_trajectory = trajData.trajectory.name;
|
|
260
|
+
s.trajectory_step_id = currentStep.id;
|
|
261
|
+
s.trajectory_step_title = currentStep.title;
|
|
262
|
+
// Override scope and categories from step
|
|
263
|
+
if (currentStep.scope)
|
|
264
|
+
s.scope = currentStep.scope;
|
|
265
|
+
if (currentStep.categories && currentStep.categories.length > 0) {
|
|
266
|
+
s.categories = currentStep.categories;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.warn(`[promptwheel] Trajectory load failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
273
|
+
}
|
|
274
|
+
const prompt = guidelinesBlock + metadataBlock + indexBlock + dedupBlock + trajectoryBlock + learningsBlock + escalationBlock + buildScoutPrompt(s.scope, s.categories, s.min_confidence, s.max_proposals_per_scout, dedupContext, formula, hints, s.eco, s.min_impact_score, s.scouted_dirs, s.scout_exclude_dirs, coverageCtx);
|
|
275
|
+
// Reset scout_retries at the start of a fresh cycle (non-retry entry)
|
|
276
|
+
if (s.scout_retries === 0) {
|
|
277
|
+
s.scout_cycles++;
|
|
278
|
+
}
|
|
279
|
+
run.appendEvent('ADVANCE_RETURNED', { phase: 'SCOUT', has_prompt: true });
|
|
280
|
+
return promptResponse(run, 'SCOUT', prompt, 'Scouting for improvements', {
|
|
281
|
+
allowed_paths: [s.scope],
|
|
282
|
+
denied_paths: [],
|
|
283
|
+
denied_patterns: [],
|
|
284
|
+
max_files: 100,
|
|
285
|
+
max_lines: 0,
|
|
286
|
+
required_commands: [],
|
|
287
|
+
plan_required: false,
|
|
288
|
+
auto_approve_patterns: getScoutAutoApprove(),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async function advanceNextTicket(ctx) {
|
|
292
|
+
const { run, db } = ctx;
|
|
293
|
+
const s = run.require();
|
|
294
|
+
// Dry-run mode — stop after scouting, don't execute tickets
|
|
295
|
+
if (s.dry_run) {
|
|
296
|
+
run.setPhase('DONE');
|
|
297
|
+
return stopResponse(run, 'DONE', 'Dry-run mode — scout complete, no tickets executed.');
|
|
298
|
+
}
|
|
299
|
+
// Ensure learnings are loaded for scope policy derivation
|
|
300
|
+
run.ensureLearningsLoaded();
|
|
301
|
+
// Check if we've hit PR limit (only when creating PRs)
|
|
302
|
+
if (s.create_prs && s.prs_created >= s.max_prs) {
|
|
303
|
+
run.setPhase('DONE');
|
|
304
|
+
return stopResponse(run, 'DONE', `PR limit reached (${s.prs_created}/${s.max_prs})`);
|
|
305
|
+
}
|
|
306
|
+
const parallelCount = s.create_prs
|
|
307
|
+
? Math.min(s.parallel, s.max_prs - s.prs_created)
|
|
308
|
+
: s.parallel;
|
|
309
|
+
// Find ready tickets
|
|
310
|
+
const readyTickets = await repos.tickets.listByProject(db, s.project_id, { status: 'ready', limit: parallelCount });
|
|
311
|
+
if (readyTickets.length === 0) {
|
|
312
|
+
// No more tickets — scout again if cycles remain, otherwise finish
|
|
313
|
+
if (s.scout_cycles < s.max_cycles) {
|
|
314
|
+
run.setPhase('SCOUT');
|
|
315
|
+
return advance(ctx);
|
|
316
|
+
}
|
|
317
|
+
run.setPhase('DONE');
|
|
318
|
+
const cov = run.buildDigest().coverage;
|
|
319
|
+
const covSuffix = cov.sectors_total > 0
|
|
320
|
+
? ` (${cov.sectors_scanned}/${cov.sectors_total} sectors scanned, ${cov.percent}% coverage)`
|
|
321
|
+
: '';
|
|
322
|
+
return stopResponse(run, 'DONE', `No more tickets to process${covSuffix}`);
|
|
323
|
+
}
|
|
324
|
+
// If parallel > 1 and multiple tickets ready → dispatch batch
|
|
325
|
+
if (parallelCount > 1 && readyTickets.length > 1) {
|
|
326
|
+
const parallelTickets = [];
|
|
327
|
+
for (const ticket of readyTickets) {
|
|
328
|
+
await repos.tickets.updateStatus(db, ticket.id, 'in_progress');
|
|
329
|
+
run.initTicketWorker(ticket.id, ticket);
|
|
330
|
+
await repos.runs.create(db, {
|
|
331
|
+
projectId: s.project_id,
|
|
332
|
+
type: 'worker',
|
|
333
|
+
ticketId: ticket.id,
|
|
334
|
+
});
|
|
335
|
+
const policy = deriveScopePolicy({
|
|
336
|
+
allowedPaths: ticket.allowedPaths ?? [],
|
|
337
|
+
category: ticket.category ?? 'refactor',
|
|
338
|
+
maxLinesPerTicket: s.max_lines_per_ticket,
|
|
339
|
+
learnings: s.cached_learnings,
|
|
340
|
+
});
|
|
341
|
+
parallelTickets.push({
|
|
342
|
+
ticket_id: ticket.id,
|
|
343
|
+
title: ticket.title,
|
|
344
|
+
description: ticket.description ?? '',
|
|
345
|
+
constraints: {
|
|
346
|
+
allowed_paths: policy.allowed_paths,
|
|
347
|
+
denied_paths: policy.denied_paths,
|
|
348
|
+
denied_patterns: policy.denied_patterns.map(r => r.source),
|
|
349
|
+
max_files: policy.max_files,
|
|
350
|
+
max_lines: policy.max_lines,
|
|
351
|
+
required_commands: ticket.verificationCommands ?? [],
|
|
352
|
+
plan_required: policy.plan_required,
|
|
353
|
+
auto_approve_patterns: getExecuteAutoApprove(ticket.category ?? null),
|
|
354
|
+
},
|
|
355
|
+
inline_prompt: '', // filled below
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
run.setPhase('PARALLEL_EXECUTE');
|
|
359
|
+
// Build inline prompts for each ticket — subagents are self-contained
|
|
360
|
+
const guidelines = loadGuidelines(ctx.project.rootPath);
|
|
361
|
+
const guidelinesBlock = guidelines ? formatGuidelinesForPrompt(guidelines) + '\n\n' : '';
|
|
362
|
+
const projectMeta = detectProjectMetadata(ctx.project.rootPath);
|
|
363
|
+
const metadataBlock = formatMetadataForPrompt(projectMeta) + '\n\n';
|
|
364
|
+
// Read project setup command and QA baseline from .promptwheel/
|
|
365
|
+
let setupCommand;
|
|
366
|
+
let baselineFailures = [];
|
|
367
|
+
try {
|
|
368
|
+
const configPath = path.join(ctx.project.rootPath, '.promptwheel', 'config.json');
|
|
369
|
+
if (fs.existsSync(configPath)) {
|
|
370
|
+
const configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
371
|
+
setupCommand = configData.setup;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
console.warn(`[promptwheel] Failed to read config.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const baselinePath = path.join(ctx.project.rootPath, '.promptwheel', 'qa-baseline.json');
|
|
379
|
+
if (fs.existsSync(baselinePath)) {
|
|
380
|
+
const data = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
381
|
+
baselineFailures = data.failures ?? [];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
console.warn(`[promptwheel] Failed to read qa-baseline.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
386
|
+
}
|
|
387
|
+
for (const pt of parallelTickets) {
|
|
388
|
+
const ticket = readyTickets.find(t => t.id === pt.ticket_id);
|
|
389
|
+
pt.inline_prompt = buildInlineTicketPrompt(ticket, pt.constraints, guidelinesBlock, metadataBlock, s.create_prs, s.draft, s.direct, setupCommand, baselineFailures);
|
|
390
|
+
}
|
|
391
|
+
// Build orchestration prompt for the main agent
|
|
392
|
+
const ticketList = parallelTickets.map((t, i) => `${i + 1}. **${t.title}** (ID: \`${t.ticket_id}\`)`).join('\n');
|
|
393
|
+
const orchestrationPrompt = [
|
|
394
|
+
`# Parallel Execution — ${parallelTickets.length} tickets`,
|
|
395
|
+
'',
|
|
396
|
+
ticketList,
|
|
397
|
+
'',
|
|
398
|
+
'## Instructions',
|
|
399
|
+
'',
|
|
400
|
+
'Use the **Task tool** to spawn one subagent per ticket. Send ALL Task calls in a **single message** for concurrency.',
|
|
401
|
+
'',
|
|
402
|
+
'For each ticket in `parallel_tickets`:',
|
|
403
|
+
'```',
|
|
404
|
+
'Task({',
|
|
405
|
+
' subagent_type: "general-purpose",',
|
|
406
|
+
' description: "Ticket: <title>",',
|
|
407
|
+
' prompt: parallel_tickets[i].inline_prompt',
|
|
408
|
+
'})',
|
|
409
|
+
'```',
|
|
410
|
+
'',
|
|
411
|
+
'The `inline_prompt` field contains everything the subagent needs — no MCP tools required.',
|
|
412
|
+
'Subagents will edit code, run tests, commit, push, and create PRs independently.',
|
|
413
|
+
'',
|
|
414
|
+
'## After All Subagents Return',
|
|
415
|
+
'',
|
|
416
|
+
'For each subagent result, call `promptwheel_ticket_event` to record the outcome:',
|
|
417
|
+
...(s.create_prs
|
|
418
|
+
? ['- Success: `type: "PR_CREATED"`, `payload: { ticket_id, url, branch }`']
|
|
419
|
+
: ['- Success: `type: "TICKET_RESULT"`, `payload: { ticket_id, status: "success", changed_files: [...] }`']),
|
|
420
|
+
'- Failure: `type: "TICKET_RESULT"`, `payload: { ticket_id, status: "failed", reason: "..." }`',
|
|
421
|
+
'',
|
|
422
|
+
'Then call `promptwheel_advance` to continue.',
|
|
423
|
+
].join('\n');
|
|
424
|
+
return {
|
|
425
|
+
next_action: 'PARALLEL_EXECUTE',
|
|
426
|
+
phase: 'PARALLEL_EXECUTE',
|
|
427
|
+
prompt: orchestrationPrompt,
|
|
428
|
+
reason: `Dispatching ${parallelTickets.length} tickets for parallel execution`,
|
|
429
|
+
constraints: emptyConstraints(),
|
|
430
|
+
digest: run.buildDigest(),
|
|
431
|
+
parallel_tickets: parallelTickets,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
// Sequential flow (parallel=1 or only 1 ticket)
|
|
435
|
+
const ticket = readyTickets[0];
|
|
436
|
+
// Assign ticket
|
|
437
|
+
await repos.tickets.updateStatus(db, ticket.id, 'in_progress');
|
|
438
|
+
run.assignTicket(ticket.id);
|
|
439
|
+
// Create worker run record
|
|
440
|
+
await repos.runs.create(db, {
|
|
441
|
+
projectId: s.project_id,
|
|
442
|
+
type: 'worker',
|
|
443
|
+
ticketId: ticket.id,
|
|
444
|
+
});
|
|
445
|
+
// Derive scope policy for this ticket
|
|
446
|
+
const policy = deriveScopePolicy({
|
|
447
|
+
allowedPaths: ticket.allowedPaths ?? [],
|
|
448
|
+
category: ticket.category ?? 'refactor',
|
|
449
|
+
maxLinesPerTicket: s.max_lines_per_ticket,
|
|
450
|
+
learnings: s.cached_learnings,
|
|
451
|
+
});
|
|
452
|
+
const constraints = {
|
|
453
|
+
allowed_paths: policy.allowed_paths,
|
|
454
|
+
denied_paths: policy.denied_paths,
|
|
455
|
+
denied_patterns: policy.denied_patterns.map(r => r.source),
|
|
456
|
+
max_files: policy.max_files,
|
|
457
|
+
max_lines: policy.max_lines,
|
|
458
|
+
required_commands: ticket.verificationCommands ?? [],
|
|
459
|
+
plan_required: policy.plan_required,
|
|
460
|
+
auto_approve_patterns: getScoutAutoApprove(),
|
|
461
|
+
};
|
|
462
|
+
// Docs category: skip plan, go straight to execute
|
|
463
|
+
if (!policy.plan_required) {
|
|
464
|
+
s.plan_approved = true;
|
|
465
|
+
run.setPhase('EXECUTE');
|
|
466
|
+
return advanceExecute(ctx);
|
|
467
|
+
}
|
|
468
|
+
// Move to PLAN phase — require commit plan before execution
|
|
469
|
+
run.setPhase('PLAN');
|
|
470
|
+
const prompt = buildPlanPrompt(ticket);
|
|
471
|
+
return promptResponse(run, 'PLAN', prompt, `Planning ticket: ${ticket.title}`, { ...constraints, auto_approve_patterns: getScoutAutoApprove() });
|
|
472
|
+
}
|
|
473
|
+
async function advancePlan(ctx) {
|
|
474
|
+
const { run, db } = ctx;
|
|
475
|
+
const s = run.require();
|
|
476
|
+
// If plan is already approved, move to execute
|
|
477
|
+
if (s.plan_approved) {
|
|
478
|
+
run.setPhase('EXECUTE');
|
|
479
|
+
return advanceExecute(ctx);
|
|
480
|
+
}
|
|
481
|
+
// If too many rejections, block the ticket
|
|
482
|
+
if (s.plan_rejections >= MAX_PLAN_REJECTIONS) {
|
|
483
|
+
run.appendEvent('TICKET_FAILED', {
|
|
484
|
+
ticket_id: s.current_ticket_id,
|
|
485
|
+
reason: 'Plan rejected too many times',
|
|
486
|
+
});
|
|
487
|
+
if (s.current_ticket_id) {
|
|
488
|
+
await repos.tickets.updateStatus(db, s.current_ticket_id, 'blocked');
|
|
489
|
+
}
|
|
490
|
+
s.tickets_blocked++;
|
|
491
|
+
s.current_ticket_id = null;
|
|
492
|
+
s.current_ticket_plan = null;
|
|
493
|
+
run.setPhase('BLOCKED_NEEDS_HUMAN');
|
|
494
|
+
return stopResponse(run, 'BLOCKED_NEEDS_HUMAN', `Commit plan rejected ${MAX_PLAN_REJECTIONS} times. Needs human review.`);
|
|
495
|
+
}
|
|
496
|
+
// Request a commit plan from the client
|
|
497
|
+
const ticket = s.current_ticket_id
|
|
498
|
+
? await repos.tickets.getById(db, s.current_ticket_id)
|
|
499
|
+
: null;
|
|
500
|
+
if (!ticket) {
|
|
501
|
+
run.setPhase('NEXT_TICKET');
|
|
502
|
+
return advance(ctx);
|
|
503
|
+
}
|
|
504
|
+
const policy = deriveScopePolicy({
|
|
505
|
+
allowedPaths: ticket.allowedPaths ?? [],
|
|
506
|
+
category: ticket.category ?? 'refactor',
|
|
507
|
+
maxLinesPerTicket: s.max_lines_per_ticket,
|
|
508
|
+
learnings: s.cached_learnings,
|
|
509
|
+
});
|
|
510
|
+
const learningsBlock = buildLearningsBlock(run, ticket.allowedPaths ?? [], ticket.verificationCommands ?? []);
|
|
511
|
+
const riskBlock = buildRiskContextBlock(policy.risk_assessment);
|
|
512
|
+
let basePlanPrompt;
|
|
513
|
+
if (s.plan_rejections > 0) {
|
|
514
|
+
const planCriticBlock = buildPlanRejectionCriticBlock({
|
|
515
|
+
rejection_reason: s.last_plan_rejection_reason ?? 'Plan did not pass scope validation',
|
|
516
|
+
attempt: s.plan_rejections + 1,
|
|
517
|
+
max_attempts: MAX_PLAN_REJECTIONS,
|
|
518
|
+
}, s.cached_learnings, ticket.allowedPaths ?? []);
|
|
519
|
+
const preamble = planCriticBlock
|
|
520
|
+
? `${planCriticBlock}\n\n`
|
|
521
|
+
: `Your previous commit plan was rejected: ${s.last_plan_rejection_reason ?? 'scope violation'}. Please revise.\n\n`;
|
|
522
|
+
basePlanPrompt = preamble + buildPlanPrompt(ticket);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
basePlanPrompt = buildPlanPrompt(ticket);
|
|
526
|
+
}
|
|
527
|
+
const prompt = learningsBlock + riskBlock + basePlanPrompt;
|
|
528
|
+
return promptResponse(run, 'PLAN', prompt, s.plan_rejections > 0
|
|
529
|
+
? `Re-planning (attempt ${s.plan_rejections + 1}/${MAX_PLAN_REJECTIONS})`
|
|
530
|
+
: `Awaiting commit plan for: ${ticket.title}`, {
|
|
531
|
+
allowed_paths: policy.allowed_paths,
|
|
532
|
+
denied_paths: policy.denied_paths,
|
|
533
|
+
denied_patterns: policy.denied_patterns.map(r => r.source),
|
|
534
|
+
max_files: policy.max_files,
|
|
535
|
+
max_lines: policy.max_lines,
|
|
536
|
+
required_commands: ticket.verificationCommands ?? [],
|
|
537
|
+
plan_required: policy.plan_required,
|
|
538
|
+
auto_approve_patterns: getScoutAutoApprove(),
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
async function advanceExecute(ctx) {
|
|
542
|
+
const { run, db } = ctx;
|
|
543
|
+
const s = run.require();
|
|
544
|
+
// Check ticket step budget
|
|
545
|
+
if (s.ticket_step_count >= s.ticket_step_budget) {
|
|
546
|
+
if (s.current_ticket_id) {
|
|
547
|
+
await repos.tickets.updateStatus(db, s.current_ticket_id, 'blocked');
|
|
548
|
+
run.failTicket('Ticket step budget exhausted');
|
|
549
|
+
}
|
|
550
|
+
run.setPhase('BLOCKED_NEEDS_HUMAN');
|
|
551
|
+
return stopResponse(run, 'BLOCKED_NEEDS_HUMAN', `Ticket step budget exhausted (${s.ticket_step_count}/${s.ticket_step_budget})`);
|
|
552
|
+
}
|
|
553
|
+
const ticket = s.current_ticket_id
|
|
554
|
+
? await repos.tickets.getById(db, s.current_ticket_id)
|
|
555
|
+
: null;
|
|
556
|
+
if (!ticket) {
|
|
557
|
+
run.setPhase('NEXT_TICKET');
|
|
558
|
+
return advance(ctx);
|
|
559
|
+
}
|
|
560
|
+
const policy = deriveScopePolicy({
|
|
561
|
+
allowedPaths: ticket.allowedPaths ?? [],
|
|
562
|
+
category: ticket.category ?? 'refactor',
|
|
563
|
+
maxLinesPerTicket: s.max_lines_per_ticket,
|
|
564
|
+
learnings: s.cached_learnings,
|
|
565
|
+
});
|
|
566
|
+
const guidelines = loadGuidelines(ctx.project.rootPath);
|
|
567
|
+
const guidelinesBlock = guidelines ? formatGuidelinesForPrompt(guidelines) + '\n\n' : '';
|
|
568
|
+
const learningsBlock = buildLearningsBlock(run, ticket.allowedPaths ?? [], ticket.verificationCommands ?? []);
|
|
569
|
+
const riskBlock = buildRiskContextBlock(policy.risk_assessment);
|
|
570
|
+
// Build critic block for QA retries
|
|
571
|
+
let criticBlock = '';
|
|
572
|
+
if (s.qa_retries > 0 && s.last_qa_failure) {
|
|
573
|
+
const failureContext = {
|
|
574
|
+
failed_commands: s.last_qa_failure.failed_commands,
|
|
575
|
+
error_output: s.last_qa_failure.error_output,
|
|
576
|
+
attempt: s.qa_retries + 1,
|
|
577
|
+
max_attempts: MAX_QA_RETRIES,
|
|
578
|
+
};
|
|
579
|
+
const risk = computeRetryRisk(ticket.allowedPaths ?? [], ticket.verificationCommands ?? [], s.cached_learnings, failureContext);
|
|
580
|
+
const strategies = scoreStrategies(ticket.allowedPaths ?? [], failureContext, s.cached_learnings);
|
|
581
|
+
criticBlock = buildCriticBlock(failureContext, risk, strategies, s.cached_learnings);
|
|
582
|
+
if (criticBlock)
|
|
583
|
+
criticBlock += '\n\n';
|
|
584
|
+
}
|
|
585
|
+
const prompt = guidelinesBlock + learningsBlock + riskBlock + criticBlock + buildExecutePrompt(ticket, s.current_ticket_plan);
|
|
586
|
+
return promptResponse(run, 'EXECUTE', prompt, `Executing ticket: ${ticket.title}`, {
|
|
587
|
+
allowed_paths: policy.allowed_paths,
|
|
588
|
+
denied_paths: policy.denied_paths,
|
|
589
|
+
denied_patterns: policy.denied_patterns.map(r => r.source),
|
|
590
|
+
max_files: policy.max_files,
|
|
591
|
+
max_lines: policy.max_lines,
|
|
592
|
+
required_commands: ticket.verificationCommands ?? [],
|
|
593
|
+
plan_required: false,
|
|
594
|
+
auto_approve_patterns: getExecuteAutoApprove(ticket.category ?? null),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async function advanceQa(ctx) {
|
|
598
|
+
const { run, db } = ctx;
|
|
599
|
+
const s = run.require();
|
|
600
|
+
// If QA retries exceeded, give up on this ticket
|
|
601
|
+
if (s.qa_retries >= MAX_QA_RETRIES) {
|
|
602
|
+
if (s.current_ticket_id) {
|
|
603
|
+
await repos.tickets.updateStatus(db, s.current_ticket_id, 'blocked');
|
|
604
|
+
run.failTicket(`QA failed ${MAX_QA_RETRIES} times`);
|
|
605
|
+
}
|
|
606
|
+
run.setPhase('NEXT_TICKET');
|
|
607
|
+
return advance(ctx);
|
|
608
|
+
}
|
|
609
|
+
const ticket = s.current_ticket_id
|
|
610
|
+
? await repos.tickets.getById(db, s.current_ticket_id)
|
|
611
|
+
: null;
|
|
612
|
+
if (!ticket) {
|
|
613
|
+
run.setPhase('NEXT_TICKET');
|
|
614
|
+
return advance(ctx);
|
|
615
|
+
}
|
|
616
|
+
// Merge session-level qa_commands with ticket verification commands
|
|
617
|
+
const ticketCommands = ticket.verificationCommands ?? [];
|
|
618
|
+
const sessionQaCommands = s.qa_commands ?? [];
|
|
619
|
+
const allCommands = [...new Set([...ticketCommands, ...sessionQaCommands])];
|
|
620
|
+
const qaTicket = allCommands.length !== ticketCommands.length
|
|
621
|
+
? { ...ticket, verificationCommands: allCommands }
|
|
622
|
+
: ticket;
|
|
623
|
+
const learningsBlock = buildLearningsBlock(run, [], allCommands);
|
|
624
|
+
const crossVerifyPreamble = s.cross_verify
|
|
625
|
+
? '## IMPORTANT — Independent Verification\n\nYou are verifying work as an INDEPENDENT verifier. Do NOT trust any prior claims of success. Run ALL commands yourself and report results honestly.\n\n'
|
|
626
|
+
: '';
|
|
627
|
+
const prompt = crossVerifyPreamble + learningsBlock + buildQaPrompt(qaTicket);
|
|
628
|
+
return promptResponse(run, 'QA', prompt, `Running QA for: ${ticket.title} (attempt ${s.qa_retries + 1}/${MAX_QA_RETRIES})`, {
|
|
629
|
+
allowed_paths: ticket.allowedPaths ?? [],
|
|
630
|
+
denied_paths: [],
|
|
631
|
+
denied_patterns: [],
|
|
632
|
+
max_files: 0,
|
|
633
|
+
max_lines: 0,
|
|
634
|
+
required_commands: allCommands,
|
|
635
|
+
plan_required: false,
|
|
636
|
+
auto_approve_patterns: getQaAutoApprove(),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
async function advancePr(ctx) {
|
|
640
|
+
const { run } = ctx;
|
|
641
|
+
const s = run.require();
|
|
642
|
+
const ticket = s.current_ticket_id
|
|
643
|
+
? await repos.tickets.getById(ctx.db, s.current_ticket_id)
|
|
644
|
+
: null;
|
|
645
|
+
const prompt = buildPrPrompt(ticket, s.draft);
|
|
646
|
+
return promptResponse(run, 'PR', prompt, 'Creating PR', {
|
|
647
|
+
allowed_paths: [],
|
|
648
|
+
denied_paths: [],
|
|
649
|
+
denied_patterns: [],
|
|
650
|
+
max_files: 0,
|
|
651
|
+
max_lines: 0,
|
|
652
|
+
required_commands: [],
|
|
653
|
+
plan_required: false,
|
|
654
|
+
auto_approve_patterns: getPrAutoApprove(),
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/** Max session-level steps a worker can go without progress before timeout */
|
|
658
|
+
const WORKER_STALL_THRESHOLD = 50;
|
|
659
|
+
async function advanceParallelExecute(ctx) {
|
|
660
|
+
const { run } = ctx;
|
|
661
|
+
const s = run.require();
|
|
662
|
+
// Timeout check: fail workers that haven't made progress
|
|
663
|
+
for (const [id, w] of Object.entries(s.ticket_workers)) {
|
|
664
|
+
const stalledFor = s.step_count - (w.last_active_at_step ?? 0);
|
|
665
|
+
if (stalledFor >= WORKER_STALL_THRESHOLD) {
|
|
666
|
+
run.appendEvent('WORKER_TIMEOUT', { ticket_id: id, stalled_for: stalledFor });
|
|
667
|
+
run.failTicketWorker(id, `Worker timeout: no progress for ${stalledFor} steps`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// Check if all ticket workers are done
|
|
671
|
+
if (run.allWorkersComplete()) {
|
|
672
|
+
run.setPhase('NEXT_TICKET');
|
|
673
|
+
return advance(ctx);
|
|
674
|
+
}
|
|
675
|
+
// Workers still active — return status
|
|
676
|
+
const activeWorkers = Object.entries(s.ticket_workers).map(([id, w]) => ({
|
|
677
|
+
ticket_id: id,
|
|
678
|
+
phase: w.phase,
|
|
679
|
+
step_count: w.step_count,
|
|
680
|
+
}));
|
|
681
|
+
return {
|
|
682
|
+
next_action: 'PROMPT',
|
|
683
|
+
phase: 'PARALLEL_EXECUTE',
|
|
684
|
+
prompt: `Parallel execution still in progress. ${activeWorkers.length} ticket(s) still active. Wait for all subagents to complete, then call promptwheel_advance again.`,
|
|
685
|
+
reason: `${activeWorkers.length} ticket workers still active`,
|
|
686
|
+
constraints: emptyConstraints(),
|
|
687
|
+
digest: run.buildDigest(),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// Budget checks
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
function checkBudgets(run) {
|
|
694
|
+
const s = run.require();
|
|
695
|
+
if (s.step_count >= s.step_budget) {
|
|
696
|
+
run.appendEvent('BUDGET_EXHAUSTED', { which: 'step_budget', value: s.step_count });
|
|
697
|
+
run.setPhase('FAILED_BUDGET');
|
|
698
|
+
return stopResponse(run, 'FAILED_BUDGET', `Step budget exhausted (${s.step_count}/${s.step_budget})`);
|
|
699
|
+
}
|
|
700
|
+
if (s.expires_at && new Date() >= new Date(s.expires_at)) {
|
|
701
|
+
run.appendEvent('BUDGET_EXHAUSTED', { which: 'time_budget' });
|
|
702
|
+
run.setPhase('FAILED_BUDGET');
|
|
703
|
+
return stopResponse(run, 'FAILED_BUDGET', 'Time budget exhausted');
|
|
704
|
+
}
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
// Response builders
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
function stopResponse(run, phase, reason) {
|
|
711
|
+
return {
|
|
712
|
+
next_action: 'STOP',
|
|
713
|
+
phase,
|
|
714
|
+
prompt: null,
|
|
715
|
+
reason,
|
|
716
|
+
constraints: emptyConstraints(),
|
|
717
|
+
digest: run.buildDigest(),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function promptResponse(run, phase, prompt, reason, constraints) {
|
|
721
|
+
return {
|
|
722
|
+
next_action: 'PROMPT',
|
|
723
|
+
phase,
|
|
724
|
+
prompt,
|
|
725
|
+
reason,
|
|
726
|
+
constraints,
|
|
727
|
+
digest: run.buildDigest(),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function emptyConstraints() {
|
|
731
|
+
return {
|
|
732
|
+
allowed_paths: [],
|
|
733
|
+
denied_paths: [],
|
|
734
|
+
denied_patterns: [],
|
|
735
|
+
max_files: 0,
|
|
736
|
+
max_lines: 0,
|
|
737
|
+
required_commands: [],
|
|
738
|
+
plan_required: false,
|
|
739
|
+
auto_approve_patterns: [],
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function terminalReason(phase) {
|
|
743
|
+
switch (phase) {
|
|
744
|
+
case 'DONE': return 'Session completed successfully';
|
|
745
|
+
case 'BLOCKED_NEEDS_HUMAN': return 'Ticket blocked, needs human review';
|
|
746
|
+
case 'FAILED_BUDGET': return 'Budget exhausted';
|
|
747
|
+
case 'FAILED_VALIDATION': return 'Validation failed';
|
|
748
|
+
case 'FAILED_SPINDLE': return 'Loop detected by spindle';
|
|
749
|
+
default: return 'Terminal state';
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
//# sourceMappingURL=advance.js.map
|