@cristochang/spec-execution 0.1.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/dist/completion.d.ts +128 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/context.d.ts +183 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/contract.d.ts +24 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/failure.d.ts +134 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/gate.d.ts +84 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1282 -0
- package/dist/runner/executor.d.ts +48 -0
- package/dist/runner/executor.d.ts.map +1 -0
- package/dist/runner/index.d.ts +10 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/ralph.d.ts +48 -0
- package/dist/runner/ralph.d.ts.map +1 -0
- package/dist/runner/session.d.ts +164 -0
- package/dist/runner/session.d.ts.map +1 -0
- package/dist/state.d.ts +141 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var EMPTY_BOUNDARIES = {
|
|
3
|
+
objective: "",
|
|
4
|
+
inclusions: [],
|
|
5
|
+
exclusions: [],
|
|
6
|
+
constraints: [],
|
|
7
|
+
maxIterations: 0
|
|
8
|
+
};
|
|
9
|
+
var EMPTY_COMPLETION = {
|
|
10
|
+
criteria: [],
|
|
11
|
+
verification: { mode: "none" }
|
|
12
|
+
};
|
|
13
|
+
var EMPTY_SNAPSHOT = {
|
|
14
|
+
id: "",
|
|
15
|
+
status: "",
|
|
16
|
+
created: ""
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/contract.ts
|
|
20
|
+
var ContractFactory;
|
|
21
|
+
((ContractFactory2) => {
|
|
22
|
+
function create(spec) {
|
|
23
|
+
if (!spec) {
|
|
24
|
+
return {
|
|
25
|
+
gate: { allowed: false, reason: "spec_not_found" },
|
|
26
|
+
boundaries: EMPTY_BOUNDARIES,
|
|
27
|
+
completion: EMPTY_COMPLETION,
|
|
28
|
+
specSnapshot: EMPTY_SNAPSHOT
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const gate = evaluateGate(spec.status);
|
|
32
|
+
const boundaries = buildBoundaries(spec);
|
|
33
|
+
const completion = buildCompletion(spec);
|
|
34
|
+
const specSnapshot = buildSnapshot(spec);
|
|
35
|
+
return {
|
|
36
|
+
gate,
|
|
37
|
+
boundaries,
|
|
38
|
+
completion,
|
|
39
|
+
specSnapshot
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
ContractFactory2.create = create;
|
|
43
|
+
function evaluateGate(status) {
|
|
44
|
+
switch (status) {
|
|
45
|
+
case "pending":
|
|
46
|
+
return { allowed: false, reason: "spec_not_approved" };
|
|
47
|
+
case "approved":
|
|
48
|
+
return { allowed: true };
|
|
49
|
+
case "implemented":
|
|
50
|
+
return { allowed: false, reason: "spec_already_implemented" };
|
|
51
|
+
case "cancelled":
|
|
52
|
+
return { allowed: false, reason: "spec_cancelled" };
|
|
53
|
+
default:
|
|
54
|
+
return { allowed: false, reason: "spec_not_approved" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function buildBoundaries(spec) {
|
|
58
|
+
return {
|
|
59
|
+
objective: spec.objective,
|
|
60
|
+
inclusions: spec.scope.inclusions,
|
|
61
|
+
exclusions: spec.scope.exclusions,
|
|
62
|
+
constraints: spec.constraints ?? [],
|
|
63
|
+
maxIterations: 50
|
|
64
|
+
// Default, can be overridden by config
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function buildCompletion(spec) {
|
|
68
|
+
const criteria = buildCriteria(spec);
|
|
69
|
+
const verification = inferVerificationMode(spec);
|
|
70
|
+
return {
|
|
71
|
+
criteria,
|
|
72
|
+
verification
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function buildCriteria(spec) {
|
|
76
|
+
return spec.successCriteria.map((description, index) => ({
|
|
77
|
+
id: `criterion_${index}`,
|
|
78
|
+
description,
|
|
79
|
+
status: "pending"
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
function inferVerificationMode(spec) {
|
|
83
|
+
const constraintsLower = spec.constraints?.map((c) => c.toLowerCase()) ?? [];
|
|
84
|
+
const inclusionsLower = spec.scope.inclusions.map((i) => i.toLowerCase());
|
|
85
|
+
const requiresTest = constraintsLower.some(
|
|
86
|
+
(c) => c.includes("test") || c.includes("ci") || c.includes("coverage")
|
|
87
|
+
);
|
|
88
|
+
if (requiresTest) {
|
|
89
|
+
return { mode: "automated" };
|
|
90
|
+
}
|
|
91
|
+
const isUI = inclusionsLower.some(
|
|
92
|
+
(s) => s.includes("ui") || s.includes("component") || s.includes("page") || s.includes("screen") || s.includes("view")
|
|
93
|
+
);
|
|
94
|
+
if (isUI) {
|
|
95
|
+
return { mode: "manual" };
|
|
96
|
+
}
|
|
97
|
+
return { mode: "none" };
|
|
98
|
+
}
|
|
99
|
+
function buildSnapshot(spec) {
|
|
100
|
+
return {
|
|
101
|
+
id: spec.id,
|
|
102
|
+
status: spec.status,
|
|
103
|
+
created: spec.created
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
})(ContractFactory || (ContractFactory = {}));
|
|
107
|
+
|
|
108
|
+
// src/gate.ts
|
|
109
|
+
var ExecutionGate;
|
|
110
|
+
((ExecutionGate2) => {
|
|
111
|
+
function check(spec, session) {
|
|
112
|
+
if (!spec) {
|
|
113
|
+
return { allowed: false, reason: "spec_not_found" };
|
|
114
|
+
}
|
|
115
|
+
if (spec.status === "cancelled") {
|
|
116
|
+
return { allowed: false, reason: "spec_cancelled" };
|
|
117
|
+
}
|
|
118
|
+
if (spec.status === "implemented") {
|
|
119
|
+
return { allowed: false, reason: "spec_already_implemented" };
|
|
120
|
+
}
|
|
121
|
+
if (spec.status !== "approved") {
|
|
122
|
+
return { allowed: false, reason: "spec_not_approved" };
|
|
123
|
+
}
|
|
124
|
+
if (session.iteration >= session.config.maxIterations) {
|
|
125
|
+
return { allowed: false, reason: "max_iterations_exceeded" };
|
|
126
|
+
}
|
|
127
|
+
if (session.isBlockedByFailures()) {
|
|
128
|
+
return { allowed: false, reason: "blocked_by_failures" };
|
|
129
|
+
}
|
|
130
|
+
const contract = ContractFactory.create(spec);
|
|
131
|
+
return {
|
|
132
|
+
allowed: true,
|
|
133
|
+
contract
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
ExecutionGate2.check = check;
|
|
137
|
+
function checkSpecOnly(spec) {
|
|
138
|
+
if (!spec) {
|
|
139
|
+
return { allowed: false, reason: "spec_not_found" };
|
|
140
|
+
}
|
|
141
|
+
if (spec.status === "cancelled") {
|
|
142
|
+
return { allowed: false, reason: "spec_cancelled" };
|
|
143
|
+
}
|
|
144
|
+
if (spec.status === "implemented") {
|
|
145
|
+
return { allowed: false, reason: "spec_already_implemented" };
|
|
146
|
+
}
|
|
147
|
+
if (spec.status !== "approved") {
|
|
148
|
+
return { allowed: false, reason: "spec_not_approved" };
|
|
149
|
+
}
|
|
150
|
+
return { allowed: true };
|
|
151
|
+
}
|
|
152
|
+
ExecutionGate2.checkSpecOnly = checkSpecOnly;
|
|
153
|
+
function isRetryableReason(reason) {
|
|
154
|
+
const retryable = /* @__PURE__ */ new Set([
|
|
155
|
+
"spec_not_approved",
|
|
156
|
+
"max_iterations_exceeded",
|
|
157
|
+
"blocked_by_failures"
|
|
158
|
+
]);
|
|
159
|
+
return retryable.has(reason);
|
|
160
|
+
}
|
|
161
|
+
ExecutionGate2.isRetryableReason = isRetryableReason;
|
|
162
|
+
function isTerminalReason(reason) {
|
|
163
|
+
const terminal = /* @__PURE__ */ new Set([
|
|
164
|
+
"spec_not_found",
|
|
165
|
+
"spec_cancelled",
|
|
166
|
+
"spec_already_implemented"
|
|
167
|
+
]);
|
|
168
|
+
return terminal.has(reason);
|
|
169
|
+
}
|
|
170
|
+
ExecutionGate2.isTerminalReason = isTerminalReason;
|
|
171
|
+
})(ExecutionGate || (ExecutionGate = {}));
|
|
172
|
+
|
|
173
|
+
// src/state.ts
|
|
174
|
+
function computeProgress(counts, total) {
|
|
175
|
+
return {
|
|
176
|
+
total,
|
|
177
|
+
pending: counts.pending ?? 0,
|
|
178
|
+
inProgress: counts.in_progress ?? 0,
|
|
179
|
+
satisfied: counts.satisfied ?? 0,
|
|
180
|
+
verified: counts.verified ?? 0,
|
|
181
|
+
blocked: counts.blocked ?? 0,
|
|
182
|
+
failed: counts.failed ?? 0,
|
|
183
|
+
percentage: total > 0 ? (counts.verified ?? 0) / total * 100 : 0
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function createToolCallSummary(tool, success, files, error) {
|
|
187
|
+
return {
|
|
188
|
+
tool,
|
|
189
|
+
success,
|
|
190
|
+
timestamp: Date.now(),
|
|
191
|
+
files: files ?? [],
|
|
192
|
+
error
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
var ExecutionState = class {
|
|
196
|
+
_filesTouched = /* @__PURE__ */ new Set();
|
|
197
|
+
_criteriaStatus = /* @__PURE__ */ new Map();
|
|
198
|
+
_criteriaVerifiedAt = /* @__PURE__ */ new Map();
|
|
199
|
+
_criteriaVerificationMethod = /* @__PURE__ */ new Map();
|
|
200
|
+
_lastToolCall;
|
|
201
|
+
_startedAt;
|
|
202
|
+
_lastUpdated;
|
|
203
|
+
constructor() {
|
|
204
|
+
this._startedAt = Date.now();
|
|
205
|
+
this._lastUpdated = this._startedAt;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Initialize criteria from a list of criterion IDs
|
|
209
|
+
*/
|
|
210
|
+
initializeCriteria(criterionIds) {
|
|
211
|
+
for (const id of criterionIds) {
|
|
212
|
+
if (!this._criteriaStatus.has(id)) {
|
|
213
|
+
this._criteriaStatus.set(id, "pending");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this._touch();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get the status of a criterion
|
|
220
|
+
*/
|
|
221
|
+
getCriterionStatus(criterionId) {
|
|
222
|
+
return this._criteriaStatus.get(criterionId);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Set the status of a criterion
|
|
226
|
+
*/
|
|
227
|
+
setCriterionStatus(criterionId, status, verifiedAt, verificationMethod) {
|
|
228
|
+
this._criteriaStatus.set(criterionId, status);
|
|
229
|
+
if (verifiedAt) {
|
|
230
|
+
this._criteriaVerifiedAt.set(criterionId, verifiedAt);
|
|
231
|
+
}
|
|
232
|
+
if (verificationMethod) {
|
|
233
|
+
this._criteriaVerificationMethod.set(criterionId, verificationMethod);
|
|
234
|
+
}
|
|
235
|
+
this._touch();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get all criteria statuses
|
|
239
|
+
*/
|
|
240
|
+
getAllCriteriaStatus() {
|
|
241
|
+
return this._criteriaStatus;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Mark a file as touched
|
|
245
|
+
*/
|
|
246
|
+
touchFile(file) {
|
|
247
|
+
this._filesTouched.add(file);
|
|
248
|
+
this._touch();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Mark multiple files as touched
|
|
252
|
+
*/
|
|
253
|
+
touchFiles(files) {
|
|
254
|
+
for (const file of files) {
|
|
255
|
+
this._filesTouched.add(file);
|
|
256
|
+
}
|
|
257
|
+
this._touch();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Check if a file has been touched
|
|
261
|
+
*/
|
|
262
|
+
hasTouchedFile(file) {
|
|
263
|
+
return this._filesTouched.has(file);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get all touched files
|
|
267
|
+
*/
|
|
268
|
+
getTouchedFiles() {
|
|
269
|
+
return Array.from(this._filesTouched);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get count of touched files
|
|
273
|
+
*/
|
|
274
|
+
getTouchedFileCount() {
|
|
275
|
+
return this._filesTouched.size;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Record a tool call
|
|
279
|
+
*/
|
|
280
|
+
recordToolCall(summary) {
|
|
281
|
+
this._lastToolCall = summary;
|
|
282
|
+
this._touch();
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get the last tool call summary
|
|
286
|
+
*/
|
|
287
|
+
getLastToolCall() {
|
|
288
|
+
return this._lastToolCall;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get progress summary for all criteria
|
|
292
|
+
*/
|
|
293
|
+
getProgress() {
|
|
294
|
+
const counts = {
|
|
295
|
+
pending: 0,
|
|
296
|
+
in_progress: 0,
|
|
297
|
+
satisfied: 0,
|
|
298
|
+
verified: 0,
|
|
299
|
+
blocked: 0,
|
|
300
|
+
failed: 0
|
|
301
|
+
};
|
|
302
|
+
for (const status of this._criteriaStatus.values()) {
|
|
303
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
304
|
+
}
|
|
305
|
+
return computeProgress(counts, this._criteriaStatus.size);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Check if all criteria are verified
|
|
309
|
+
*/
|
|
310
|
+
isAllVerified() {
|
|
311
|
+
if (this._criteriaStatus.size === 0) return false;
|
|
312
|
+
for (const status of this._criteriaStatus.values()) {
|
|
313
|
+
if (status !== "verified") return false;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get criteria with a specific status
|
|
319
|
+
*/
|
|
320
|
+
getCriteriaByStatus(status) {
|
|
321
|
+
const result = [];
|
|
322
|
+
for (const [id, s] of this._criteriaStatus) {
|
|
323
|
+
if (s === status) {
|
|
324
|
+
result.push(id);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get the next pending criterion
|
|
331
|
+
*/
|
|
332
|
+
getNextPendingCriterion() {
|
|
333
|
+
for (const [id, status] of this._criteriaStatus) {
|
|
334
|
+
if (status === "pending") {
|
|
335
|
+
return id;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Get session duration in milliseconds
|
|
342
|
+
*/
|
|
343
|
+
getDuration() {
|
|
344
|
+
return this._lastUpdated - this._startedAt;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Create a snapshot of the current state
|
|
348
|
+
*/
|
|
349
|
+
snapshot() {
|
|
350
|
+
return {
|
|
351
|
+
filesTouched: Array.from(this._filesTouched),
|
|
352
|
+
criteriaStatus: Object.fromEntries(this._criteriaStatus),
|
|
353
|
+
lastToolCall: this._lastToolCall,
|
|
354
|
+
startedAt: this._startedAt,
|
|
355
|
+
lastUpdated: this._lastUpdated,
|
|
356
|
+
duration: this.getDuration()
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Update internal timestamp
|
|
361
|
+
*/
|
|
362
|
+
_touch() {
|
|
363
|
+
this._lastUpdated = Date.now();
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
function restoreFromSnapshot(snapshot) {
|
|
367
|
+
const state = new ExecutionState();
|
|
368
|
+
const accessor = state;
|
|
369
|
+
accessor._startedAt = snapshot.startedAt;
|
|
370
|
+
accessor._lastUpdated = snapshot.lastUpdated;
|
|
371
|
+
for (const file of snapshot.filesTouched) {
|
|
372
|
+
state.touchFile(file);
|
|
373
|
+
}
|
|
374
|
+
for (const [id, status] of Object.entries(snapshot.criteriaStatus)) {
|
|
375
|
+
state.setCriterionStatus(id, status);
|
|
376
|
+
}
|
|
377
|
+
if (snapshot.lastToolCall) {
|
|
378
|
+
state.recordToolCall(snapshot.lastToolCall);
|
|
379
|
+
}
|
|
380
|
+
return state;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/failure.ts
|
|
384
|
+
var DEFAULT_FAILURE_CONFIG = {
|
|
385
|
+
capacity: 10,
|
|
386
|
+
retryThreshold: 3
|
|
387
|
+
};
|
|
388
|
+
var PatternExtractor;
|
|
389
|
+
((PatternExtractor2) => {
|
|
390
|
+
function fromToolOutput(tool, output) {
|
|
391
|
+
const normalizedTool = normalizeToolName(tool);
|
|
392
|
+
const recognizedTools = ["typescript", "tsc", "test", "jest", "bun", "bash", "write", "edit"];
|
|
393
|
+
if (!recognizedTools.includes(normalizedTool)) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
if (output == null) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
if (typeof output === "string") {
|
|
400
|
+
return extractFromString(normalizedTool, output);
|
|
401
|
+
}
|
|
402
|
+
if (typeof output === "object") {
|
|
403
|
+
return extractFromObject(normalizedTool, output);
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
PatternExtractor2.fromToolOutput = fromToolOutput;
|
|
408
|
+
function normalizeToolName(tool) {
|
|
409
|
+
const aliases = {
|
|
410
|
+
"type_error": "typescript",
|
|
411
|
+
"test_failure": "test",
|
|
412
|
+
"runtime_error": "bash"
|
|
413
|
+
};
|
|
414
|
+
return aliases[tool] ?? tool;
|
|
415
|
+
}
|
|
416
|
+
function shouldRecord(pattern) {
|
|
417
|
+
return pattern !== null;
|
|
418
|
+
}
|
|
419
|
+
PatternExtractor2.shouldRecord = shouldRecord;
|
|
420
|
+
function extractHint(output) {
|
|
421
|
+
if (typeof output === "string" && output.length > 0) {
|
|
422
|
+
return output.slice(0, 100);
|
|
423
|
+
}
|
|
424
|
+
if (typeof output === "object" && output !== null) {
|
|
425
|
+
if ("message" in output && typeof output.message === "string") {
|
|
426
|
+
return output.message.slice(0, 100);
|
|
427
|
+
}
|
|
428
|
+
if ("error" in output && typeof output.error === "string") {
|
|
429
|
+
return output.error.slice(0, 100);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return void 0;
|
|
433
|
+
}
|
|
434
|
+
PatternExtractor2.extractHint = extractHint;
|
|
435
|
+
function extractFromString(tool, output) {
|
|
436
|
+
const lower = output.toLowerCase();
|
|
437
|
+
if (lower.includes("type error") || lower.includes("ts(") || lower.includes("cannot find type") || lower.includes("not assignable") || lower.includes("type '") && lower.includes("' is not assignable")) {
|
|
438
|
+
return "type_error";
|
|
439
|
+
}
|
|
440
|
+
if (lower.includes("cannot find module") || lower.includes("module not found") || lower.includes("missing import")) {
|
|
441
|
+
return "missing_import";
|
|
442
|
+
}
|
|
443
|
+
if (lower.includes("syntax error") || lower.includes("unexpected token")) {
|
|
444
|
+
return "syntax_error";
|
|
445
|
+
}
|
|
446
|
+
if (lower.includes("enoent") || lower.includes("no such file")) {
|
|
447
|
+
return "file_not_found";
|
|
448
|
+
}
|
|
449
|
+
if (tool === "test" || tool === "jest" || tool === "bun") {
|
|
450
|
+
if (lower.includes("fail") || lower.includes("error") || lower.includes("assertion")) {
|
|
451
|
+
return "test_failure";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
function extractFromObject(tool, output) {
|
|
457
|
+
const o = output;
|
|
458
|
+
if ("code" in o && typeof o.code === "string") {
|
|
459
|
+
const code = o.code.toLowerCase();
|
|
460
|
+
if (code === "enoent") return "file_not_found";
|
|
461
|
+
if (code === "eacces" || code === "eperm") return "permission_denied";
|
|
462
|
+
}
|
|
463
|
+
if ("message" in o && typeof o.message === "string") {
|
|
464
|
+
return extractFromString(tool, o.message);
|
|
465
|
+
}
|
|
466
|
+
if ("error" in o) {
|
|
467
|
+
if (typeof o.error === "string") {
|
|
468
|
+
return extractFromString(tool, o.error);
|
|
469
|
+
}
|
|
470
|
+
if (typeof o.error === "object" && o.error !== null) {
|
|
471
|
+
return extractFromObject(tool, o.error);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if ("failed" in o && typeof o.failed === "number" && o.failed > 0) {
|
|
475
|
+
return "test_failure";
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
})(PatternExtractor || (PatternExtractor = {}));
|
|
480
|
+
var FailureMemory = class {
|
|
481
|
+
_config;
|
|
482
|
+
_entries;
|
|
483
|
+
constructor(config = DEFAULT_FAILURE_CONFIG) {
|
|
484
|
+
this._config = config;
|
|
485
|
+
this._entries = /* @__PURE__ */ new Map();
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Record a failure
|
|
489
|
+
*
|
|
490
|
+
* If the same failure (same criterion + pattern) already exists,
|
|
491
|
+
* increments the count. Otherwise, adds a new entry.
|
|
492
|
+
*/
|
|
493
|
+
record(input) {
|
|
494
|
+
if (!PatternExtractor.shouldRecord(input.pattern)) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const key = this._makeKey(input);
|
|
498
|
+
const existing = this._entries.get(key);
|
|
499
|
+
if (existing) {
|
|
500
|
+
const updated = {
|
|
501
|
+
...existing,
|
|
502
|
+
count: existing.count + 1,
|
|
503
|
+
lastSeen: Date.now()
|
|
504
|
+
};
|
|
505
|
+
this._entries.set(key, updated);
|
|
506
|
+
} else {
|
|
507
|
+
if (this._entries.size >= this._config.capacity) {
|
|
508
|
+
const lruKey = this._findLRU();
|
|
509
|
+
if (lruKey) {
|
|
510
|
+
this._entries.delete(lruKey);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const entry = {
|
|
514
|
+
id: `failure_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
515
|
+
criterionId: input.criterionId,
|
|
516
|
+
pattern: input.pattern,
|
|
517
|
+
hint: input.hint,
|
|
518
|
+
count: 1,
|
|
519
|
+
firstSeen: Date.now(),
|
|
520
|
+
lastSeen: Date.now()
|
|
521
|
+
};
|
|
522
|
+
this._entries.set(key, entry);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Check if a criterion should be skipped due to repeated failures
|
|
527
|
+
*
|
|
528
|
+
* Returns true if the same failure has occurred >= retryThreshold times.
|
|
529
|
+
*/
|
|
530
|
+
shouldSkip(criterionId, pattern) {
|
|
531
|
+
const key = this._makeKey({ criterionId, pattern });
|
|
532
|
+
const entry = this._entries.get(key);
|
|
533
|
+
return entry ? entry.count >= this._config.retryThreshold : false;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Get failure count for a specific criterion + pattern
|
|
537
|
+
*/
|
|
538
|
+
getFailureCount(criterionId, pattern) {
|
|
539
|
+
const key = this._makeKey({ criterionId, pattern });
|
|
540
|
+
return this._entries.get(key)?.count ?? 0;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Get all failures for a criterion
|
|
544
|
+
*/
|
|
545
|
+
getFailuresForCriterion(criterionId) {
|
|
546
|
+
const result = [];
|
|
547
|
+
for (const [key, entry] of this._entries) {
|
|
548
|
+
if (key.startsWith(`${criterionId}:`)) {
|
|
549
|
+
result.push(entry);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return result.sort((a, b) => b.lastSeen - a.lastSeen);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Get compressed failure summary
|
|
556
|
+
*/
|
|
557
|
+
summarize() {
|
|
558
|
+
const entries = Array.from(this._entries.values());
|
|
559
|
+
const patterns = [...new Set(entries.map((e) => e.pattern))];
|
|
560
|
+
return {
|
|
561
|
+
total: this._entries.size,
|
|
562
|
+
patterns,
|
|
563
|
+
recent: entries.sort((a, b) => b.lastSeen - a.lastSeen).slice(0, 3)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Clear all entries
|
|
568
|
+
*/
|
|
569
|
+
clear() {
|
|
570
|
+
this._entries.clear();
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Get total entry count
|
|
574
|
+
*/
|
|
575
|
+
get size() {
|
|
576
|
+
return this._entries.size;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Create key for storing entries
|
|
580
|
+
*/
|
|
581
|
+
_makeKey(input) {
|
|
582
|
+
return `${input.criterionId}:${input.pattern}`;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Find least recently used entry
|
|
586
|
+
*/
|
|
587
|
+
_findLRU() {
|
|
588
|
+
let lruKey = null;
|
|
589
|
+
let oldestTime = Infinity;
|
|
590
|
+
for (const [key, entry] of this._entries) {
|
|
591
|
+
if (entry.lastSeen < oldestTime) {
|
|
592
|
+
oldestTime = entry.lastSeen;
|
|
593
|
+
lruKey = key;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return lruKey;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// src/context.ts
|
|
601
|
+
var DEFAULT_ASSEMBLE_OPTIONS = {
|
|
602
|
+
maxFiles: 5,
|
|
603
|
+
maxFailures: 3,
|
|
604
|
+
includeCodeSnippet: false,
|
|
605
|
+
maxCodeLines: 100
|
|
606
|
+
};
|
|
607
|
+
function createWorkingFocus(criterionId, criterionText, targetFile, contextSnippet) {
|
|
608
|
+
return {
|
|
609
|
+
targetCriterionId: criterionId,
|
|
610
|
+
targetCriterionText: criterionText,
|
|
611
|
+
targetFile,
|
|
612
|
+
contextSnippet
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
var ContextAssembler;
|
|
616
|
+
((ContextAssembler2) => {
|
|
617
|
+
function assemble(input, options = DEFAULT_ASSEMBLE_OPTIONS) {
|
|
618
|
+
const opts = { ...DEFAULT_ASSEMBLE_OPTIONS, ...options };
|
|
619
|
+
return {
|
|
620
|
+
immutable: assembleImmutable(input),
|
|
621
|
+
state: assembleState(input, opts),
|
|
622
|
+
failures: assembleFailures(input, opts),
|
|
623
|
+
working: assembleWorking(input)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
ContextAssembler2.assemble = assemble;
|
|
627
|
+
function assembleImmutable(input) {
|
|
628
|
+
const { boundaries, completion } = input.contract;
|
|
629
|
+
return {
|
|
630
|
+
spec: {
|
|
631
|
+
objective: boundaries.objective,
|
|
632
|
+
scope: {
|
|
633
|
+
in: boundaries.inclusions,
|
|
634
|
+
out: boundaries.exclusions
|
|
635
|
+
},
|
|
636
|
+
criteria: completion.criteria.map((c) => ({
|
|
637
|
+
id: c.id,
|
|
638
|
+
text: c.description
|
|
639
|
+
})),
|
|
640
|
+
constraints: boundaries.constraints
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function assembleState(input, options) {
|
|
645
|
+
const { iteration, progress, touchedFiles, criteriaStatus } = input;
|
|
646
|
+
const maxFiles = options.maxFiles ?? 5;
|
|
647
|
+
const fileList = touchedFiles.slice(0, maxFiles);
|
|
648
|
+
const moreCount = Math.max(0, touchedFiles.length - maxFiles);
|
|
649
|
+
const displayList = moreCount > 0 ? [...fileList, `... and ${moreCount} more`] : fileList;
|
|
650
|
+
const criteriaEntries = Array.from(criteriaStatus.entries()).map(([id, status]) => ({ id, status }));
|
|
651
|
+
return {
|
|
652
|
+
iteration,
|
|
653
|
+
progress: {
|
|
654
|
+
current: progress.verified,
|
|
655
|
+
total: progress.total,
|
|
656
|
+
percentage: Math.round(progress.percentage)
|
|
657
|
+
},
|
|
658
|
+
files: {
|
|
659
|
+
count: touchedFiles.length,
|
|
660
|
+
list: displayList
|
|
661
|
+
},
|
|
662
|
+
criteria: criteriaEntries
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function assembleFailures(input, options) {
|
|
666
|
+
const { failureSummary } = input;
|
|
667
|
+
const maxFailures = options.maxFailures ?? 3;
|
|
668
|
+
const recent = failureSummary.recent.slice(0, maxFailures).map((entry) => ({
|
|
669
|
+
criterion: entry.criterionId,
|
|
670
|
+
error: entry.pattern,
|
|
671
|
+
hint: entry.hint
|
|
672
|
+
}));
|
|
673
|
+
return {
|
|
674
|
+
count: failureSummary.total,
|
|
675
|
+
patterns: failureSummary.patterns,
|
|
676
|
+
recent
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function assembleWorking(input) {
|
|
680
|
+
const { currentFocus, lastToolCall } = input;
|
|
681
|
+
if (!currentFocus) {
|
|
682
|
+
return {
|
|
683
|
+
task: "Continue working on the next criterion",
|
|
684
|
+
criterion: "No specific criterion selected",
|
|
685
|
+
lastResult: lastToolCall?.success ? `Last tool (${lastToolCall.tool}) succeeded` : lastToolCall?.error ? `Last tool (${lastToolCall.tool}) failed: ${lastToolCall.error}` : void 0
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
task: `Work on criterion: ${currentFocus.targetCriterionText}`,
|
|
690
|
+
criterion: currentFocus.targetCriterionText,
|
|
691
|
+
file: currentFocus.targetFile,
|
|
692
|
+
lastResult: lastToolCall?.success ? `Last tool (${lastToolCall.tool}) succeeded` : lastToolCall?.error ? `Last tool (${lastToolCall.tool}) failed: ${lastToolCall.error}` : void 0
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function estimateTokens(context) {
|
|
696
|
+
const json = JSON.stringify(context);
|
|
697
|
+
return Math.ceil(json.length / 4);
|
|
698
|
+
}
|
|
699
|
+
ContextAssembler2.estimateTokens = estimateTokens;
|
|
700
|
+
function formatAsText(context) {
|
|
701
|
+
const lines = [];
|
|
702
|
+
lines.push("=== Immutable Truth ===");
|
|
703
|
+
lines.push(`Objective: ${context.immutable.spec.objective}`);
|
|
704
|
+
lines.push(`In scope: ${context.immutable.spec.scope.in.join(", ")}`);
|
|
705
|
+
lines.push(`Out of scope: ${context.immutable.spec.scope.out.join(", ") || "none"}`);
|
|
706
|
+
lines.push(`Criteria (${context.immutable.spec.criteria.length}):`);
|
|
707
|
+
for (const c of context.immutable.spec.criteria) {
|
|
708
|
+
lines.push(` - ${c.id}: ${c.text}`);
|
|
709
|
+
}
|
|
710
|
+
lines.push("\n=== Execution State ===");
|
|
711
|
+
lines.push(`Iteration: ${context.state.iteration}`);
|
|
712
|
+
lines.push(`Progress: ${context.state.progress.current}/${context.state.progress.total} (${context.state.progress.percentage}%)`);
|
|
713
|
+
lines.push(`Files touched: ${context.state.files.count}`);
|
|
714
|
+
if (context.state.files.list.length > 0) {
|
|
715
|
+
lines.push(` ${context.state.files.list.join(", ")}`);
|
|
716
|
+
}
|
|
717
|
+
if (context.failures.count > 0) {
|
|
718
|
+
lines.push("\n=== Failure Memory ===");
|
|
719
|
+
lines.push(`Total failures: ${context.failures.count}`);
|
|
720
|
+
lines.push(`Patterns: ${context.failures.patterns.join(", ")}`);
|
|
721
|
+
for (const f of context.failures.recent) {
|
|
722
|
+
lines.push(` - ${f.criterion}: ${f.error}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
lines.push("\n=== Working Context ===");
|
|
726
|
+
lines.push(`Task: ${context.working.task}`);
|
|
727
|
+
if (context.working.file) {
|
|
728
|
+
lines.push(`File: ${context.working.file}`);
|
|
729
|
+
}
|
|
730
|
+
if (context.working.lastResult) {
|
|
731
|
+
lines.push(`Last result: ${context.working.lastResult}`);
|
|
732
|
+
}
|
|
733
|
+
return lines.join("\n");
|
|
734
|
+
}
|
|
735
|
+
ContextAssembler2.formatAsText = formatAsText;
|
|
736
|
+
})(ContextAssembler || (ContextAssembler = {}));
|
|
737
|
+
|
|
738
|
+
// src/completion.ts
|
|
739
|
+
function createEvidence(criteriaStatus, filesModified, testResults, verificationSummary) {
|
|
740
|
+
return {
|
|
741
|
+
criteriaStatus,
|
|
742
|
+
filesModified,
|
|
743
|
+
testResults,
|
|
744
|
+
verificationSummary,
|
|
745
|
+
timestamp: Date.now()
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function createRecommendation(specID, ready, confidence, evidence, suggestedNextStep, reason, requiresUserConfirmation = false) {
|
|
749
|
+
return {
|
|
750
|
+
specID,
|
|
751
|
+
ready,
|
|
752
|
+
confidence,
|
|
753
|
+
evidence,
|
|
754
|
+
suggestedNextStep,
|
|
755
|
+
reason,
|
|
756
|
+
requiresUserConfirmation
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function createProposal(recommendation) {
|
|
760
|
+
const type = recommendation.ready ? "completion" : recommendation.confidence === "low" ? "failed" : "partial";
|
|
761
|
+
return {
|
|
762
|
+
specID: recommendation.specID,
|
|
763
|
+
type,
|
|
764
|
+
content: {
|
|
765
|
+
evidence: recommendation.evidence,
|
|
766
|
+
summary: generateSummary(recommendation),
|
|
767
|
+
confidence: recommendation.confidence,
|
|
768
|
+
suggestedAction: recommendation.ready ? {
|
|
769
|
+
tool: "spec_update",
|
|
770
|
+
params: {
|
|
771
|
+
specID: recommendation.specID,
|
|
772
|
+
changes: recommendation.evidence.filesModified,
|
|
773
|
+
verification: recommendation.evidence.verificationSummary
|
|
774
|
+
}
|
|
775
|
+
} : void 0
|
|
776
|
+
},
|
|
777
|
+
nextStep: {
|
|
778
|
+
action: recommendation.suggestedNextStep,
|
|
779
|
+
reason: recommendation.reason,
|
|
780
|
+
requiresUserConfirmation: recommendation.requiresUserConfirmation
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function generateSummary(rec) {
|
|
785
|
+
const verifiedCount = Array.from(rec.evidence.criteriaStatus.values()).filter((s) => s === "verified").length;
|
|
786
|
+
const totalCount = rec.evidence.criteriaStatus.size;
|
|
787
|
+
if (rec.ready) {
|
|
788
|
+
return `All ${totalCount} criteria verified. Ready to mark as implemented.`;
|
|
789
|
+
}
|
|
790
|
+
return `${verifiedCount}/${totalCount} criteria verified. ${rec.suggestedNextStep === "continue" ? "Continuing execution..." : rec.reason}`;
|
|
791
|
+
}
|
|
792
|
+
var CompletionDetector;
|
|
793
|
+
((CompletionDetector2) => {
|
|
794
|
+
function evaluate(spec, progress, evidence, verificationMode) {
|
|
795
|
+
const allSatisfied = checkAllCriteriaSatisfied(progress);
|
|
796
|
+
if (!allSatisfied) {
|
|
797
|
+
return createRecommendation(
|
|
798
|
+
spec.id,
|
|
799
|
+
false,
|
|
800
|
+
"low",
|
|
801
|
+
evidence,
|
|
802
|
+
"continue",
|
|
803
|
+
`${progress.verified}/${progress.total} criteria verified. Continue execution.`,
|
|
804
|
+
false
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
switch (verificationMode) {
|
|
808
|
+
case "none":
|
|
809
|
+
return createRecommendation(
|
|
810
|
+
spec.id,
|
|
811
|
+
true,
|
|
812
|
+
"high",
|
|
813
|
+
evidence,
|
|
814
|
+
"mark_implemented",
|
|
815
|
+
"All criteria satisfied. No verification required.",
|
|
816
|
+
false
|
|
817
|
+
);
|
|
818
|
+
case "automated":
|
|
819
|
+
if (evidence.testResults?.passed) {
|
|
820
|
+
return createRecommendation(
|
|
821
|
+
spec.id,
|
|
822
|
+
true,
|
|
823
|
+
"high",
|
|
824
|
+
evidence,
|
|
825
|
+
"mark_implemented",
|
|
826
|
+
`All criteria satisfied and tests passing (${evidence.testResults.passedCount}/${evidence.testResults.total}).`,
|
|
827
|
+
false
|
|
828
|
+
);
|
|
829
|
+
} else if (evidence.testResults) {
|
|
830
|
+
return createRecommendation(
|
|
831
|
+
spec.id,
|
|
832
|
+
false,
|
|
833
|
+
"medium",
|
|
834
|
+
evidence,
|
|
835
|
+
"needs_verification",
|
|
836
|
+
`All criteria satisfied but tests failed (${evidence.testResults.failed} failures).`,
|
|
837
|
+
false
|
|
838
|
+
);
|
|
839
|
+
} else {
|
|
840
|
+
return createRecommendation(
|
|
841
|
+
spec.id,
|
|
842
|
+
false,
|
|
843
|
+
"medium",
|
|
844
|
+
evidence,
|
|
845
|
+
"needs_verification",
|
|
846
|
+
"All criteria satisfied but no test results yet.",
|
|
847
|
+
false
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
case "manual":
|
|
851
|
+
return createRecommendation(
|
|
852
|
+
spec.id,
|
|
853
|
+
true,
|
|
854
|
+
"medium",
|
|
855
|
+
evidence,
|
|
856
|
+
"needs_user_review",
|
|
857
|
+
"All criteria satisfied. Manual review required before marking as implemented.",
|
|
858
|
+
true
|
|
859
|
+
// Requires user confirmation
|
|
860
|
+
);
|
|
861
|
+
default:
|
|
862
|
+
return createRecommendation(
|
|
863
|
+
spec.id,
|
|
864
|
+
false,
|
|
865
|
+
"low",
|
|
866
|
+
evidence,
|
|
867
|
+
"continue",
|
|
868
|
+
"Unknown verification mode. Continue execution.",
|
|
869
|
+
false
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
CompletionDetector2.evaluate = evaluate;
|
|
874
|
+
function checkAllCriteriaSatisfied(progress) {
|
|
875
|
+
const completedCount = progress.satisfied + progress.verified;
|
|
876
|
+
return completedCount === progress.total && progress.failed === 0;
|
|
877
|
+
}
|
|
878
|
+
function isComplete(evidence) {
|
|
879
|
+
if (evidence.criteriaStatus.size === 0) return false;
|
|
880
|
+
for (const status of evidence.criteriaStatus.values()) {
|
|
881
|
+
if (status !== "verified") return false;
|
|
882
|
+
}
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
CompletionDetector2.isComplete = isComplete;
|
|
886
|
+
function canContinue(evidence) {
|
|
887
|
+
return !isComplete(evidence);
|
|
888
|
+
}
|
|
889
|
+
CompletionDetector2.canContinue = canContinue;
|
|
890
|
+
})(CompletionDetector || (CompletionDetector = {}));
|
|
891
|
+
|
|
892
|
+
// src/runner/executor.ts
|
|
893
|
+
function createSuccessResult(output, tool, filesModified) {
|
|
894
|
+
return {
|
|
895
|
+
success: true,
|
|
896
|
+
output,
|
|
897
|
+
tool,
|
|
898
|
+
filesModified
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function createFailureResult(error, tool) {
|
|
902
|
+
return {
|
|
903
|
+
success: false,
|
|
904
|
+
error,
|
|
905
|
+
tool
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function isExecutorAvailable(executor) {
|
|
909
|
+
return typeof executor === "object" && executor !== null && "execute" in executor && typeof executor.execute === "function";
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/runner/ralph.ts
|
|
913
|
+
var DEFAULT_RALPH_CONFIG = {
|
|
914
|
+
command: "ralph",
|
|
915
|
+
args: [],
|
|
916
|
+
timeout: 12e4
|
|
917
|
+
};
|
|
918
|
+
var RalphAdapter = class {
|
|
919
|
+
_config;
|
|
920
|
+
constructor(config = DEFAULT_RALPH_CONFIG) {
|
|
921
|
+
this._config = { ...DEFAULT_RALPH_CONFIG, ...config };
|
|
922
|
+
}
|
|
923
|
+
async execute(context) {
|
|
924
|
+
const startTime = Date.now();
|
|
925
|
+
try {
|
|
926
|
+
const prompt = ContextAssembler.formatAsText(context);
|
|
927
|
+
void this._config;
|
|
928
|
+
const ralphResult = await this._callRalph({ prompt, tools: ["edit", "write", "bash", "test"], maxIterations: 50 });
|
|
929
|
+
const duration = Date.now() - startTime;
|
|
930
|
+
return {
|
|
931
|
+
success: ralphResult.success,
|
|
932
|
+
output: ralphResult.output,
|
|
933
|
+
error: ralphResult.error,
|
|
934
|
+
tool: ralphResult.lastTool,
|
|
935
|
+
filesModified: ralphResult.filesModified,
|
|
936
|
+
duration
|
|
937
|
+
};
|
|
938
|
+
} catch (error) {
|
|
939
|
+
return { success: false, error, duration: Date.now() - startTime };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Call Ralph - IMPLEMENT THIS based on your Ralph setup
|
|
944
|
+
*
|
|
945
|
+
* Examples:
|
|
946
|
+
* - Spawn Ralph as child process
|
|
947
|
+
* - Call Ralph's HTTP API
|
|
948
|
+
* - Import Ralph module directly
|
|
949
|
+
*/
|
|
950
|
+
async _callRalph(_input) {
|
|
951
|
+
throw new Error(
|
|
952
|
+
"Ralph adapter not connected. Implement _callRalph() based on your Ralph setup.\nExamples: fetch(this._config.apiUrl, {...}), spawn(this._config.command, {...}), etc."
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
function createRalphAdapter(config) {
|
|
957
|
+
return new RalphAdapter(config);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/runner/session.ts
|
|
961
|
+
var ExecutionSession = class {
|
|
962
|
+
_config;
|
|
963
|
+
_spec;
|
|
964
|
+
_state;
|
|
965
|
+
_failures;
|
|
966
|
+
_contract;
|
|
967
|
+
_sessionStatus = "idle";
|
|
968
|
+
_iteration = 0;
|
|
969
|
+
_startedAt = 0;
|
|
970
|
+
_lastUpdate = 0;
|
|
971
|
+
_completionRecommendation = null;
|
|
972
|
+
constructor(spec, config) {
|
|
973
|
+
this._spec = spec;
|
|
974
|
+
this._config = config;
|
|
975
|
+
this._state = new ExecutionState();
|
|
976
|
+
this._failures = new FailureMemory(config.failureMemory);
|
|
977
|
+
this._contract = spec ? ContractFactory.create(spec) : null;
|
|
978
|
+
if (this._contract?.completion.criteria) {
|
|
979
|
+
const criteriaIds = this._contract.completion.criteria.map((c) => c.id);
|
|
980
|
+
this._state.initializeCriteria(criteriaIds);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// ========================================================================
|
|
984
|
+
// Public API
|
|
985
|
+
// ========================================================================
|
|
986
|
+
/**
|
|
987
|
+
* Get the current session state
|
|
988
|
+
*/
|
|
989
|
+
getSessionState() {
|
|
990
|
+
return {
|
|
991
|
+
iteration: this._iteration,
|
|
992
|
+
status: this._sessionStatus,
|
|
993
|
+
startedAt: this._startedAt,
|
|
994
|
+
lastUpdate: this._lastUpdate
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Get the completion recommendation (if any)
|
|
999
|
+
*/
|
|
1000
|
+
getCompletionRecommendation() {
|
|
1001
|
+
return this._completionRecommendation;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Get the execution contract
|
|
1005
|
+
*/
|
|
1006
|
+
getContract() {
|
|
1007
|
+
return this._contract;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Get the execution state
|
|
1011
|
+
*/
|
|
1012
|
+
getExecutionState() {
|
|
1013
|
+
return this._state;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Get the failure memory
|
|
1017
|
+
*/
|
|
1018
|
+
getFailureMemory() {
|
|
1019
|
+
return this._failures;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Check if session is blocked by failures (for gate check)
|
|
1023
|
+
*/
|
|
1024
|
+
isBlockedByFailures() {
|
|
1025
|
+
const criteria = this._state.getAllCriteriaStatus();
|
|
1026
|
+
for (const [criterionId] of criteria.entries()) {
|
|
1027
|
+
const criterionFailures = this._failures.getFailuresForCriterion(criterionId);
|
|
1028
|
+
for (const failure of criterionFailures) {
|
|
1029
|
+
if (failure.count >= this._config.failureMemory.retryThreshold) {
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Run one execution iteration
|
|
1038
|
+
*
|
|
1039
|
+
* This is the core loop method:
|
|
1040
|
+
* 1. Gate check
|
|
1041
|
+
* 2. Context assembly
|
|
1042
|
+
* 3. Executor execution
|
|
1043
|
+
* 4. Result recording
|
|
1044
|
+
* 5. Completion detection
|
|
1045
|
+
*
|
|
1046
|
+
* @returns Result of the iteration
|
|
1047
|
+
*/
|
|
1048
|
+
async runIteration() {
|
|
1049
|
+
this._lastUpdate = Date.now();
|
|
1050
|
+
if (this._iteration === 0) {
|
|
1051
|
+
this._startedAt = this._lastUpdate;
|
|
1052
|
+
}
|
|
1053
|
+
const gateResult = this._checkGate();
|
|
1054
|
+
if (!gateResult.allowed) {
|
|
1055
|
+
this._sessionStatus = this._mapGateReasonToStatus(gateResult.reason);
|
|
1056
|
+
return {
|
|
1057
|
+
continue: false,
|
|
1058
|
+
reason: gateResult.reason ?? "unknown",
|
|
1059
|
+
gateResult
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
this._sessionStatus = "running";
|
|
1063
|
+
const context = this._assembleContext();
|
|
1064
|
+
const executorResult = await this._execute(context);
|
|
1065
|
+
this._recordResult(executorResult);
|
|
1066
|
+
const completion = this._checkCompletion();
|
|
1067
|
+
this._iteration++;
|
|
1068
|
+
if (completion.ready) {
|
|
1069
|
+
this._sessionStatus = "completed";
|
|
1070
|
+
this._completionRecommendation = completion;
|
|
1071
|
+
return {
|
|
1072
|
+
continue: false,
|
|
1073
|
+
reason: "complete",
|
|
1074
|
+
completion
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
continue: true,
|
|
1079
|
+
executorResult,
|
|
1080
|
+
progress: this._state.getProgress()
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Run the full execution loop until completion
|
|
1085
|
+
*
|
|
1086
|
+
* @returns Final session state
|
|
1087
|
+
*/
|
|
1088
|
+
async run() {
|
|
1089
|
+
while (true) {
|
|
1090
|
+
const result = await this.runIteration();
|
|
1091
|
+
if (!result.continue) {
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
if (this._iteration >= this._config.maxIterations) {
|
|
1095
|
+
this._sessionStatus = "blocked";
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return this.getSessionState();
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Abort the session
|
|
1103
|
+
*/
|
|
1104
|
+
abort() {
|
|
1105
|
+
this._sessionStatus = "aborted";
|
|
1106
|
+
this._lastUpdate = Date.now();
|
|
1107
|
+
}
|
|
1108
|
+
// ========================================================================
|
|
1109
|
+
// Private Methods
|
|
1110
|
+
// ========================================================================
|
|
1111
|
+
/**
|
|
1112
|
+
* Step 1: Gate check
|
|
1113
|
+
*/
|
|
1114
|
+
_checkGate() {
|
|
1115
|
+
if (!this._spec) {
|
|
1116
|
+
return { allowed: false, reason: "spec_not_found" };
|
|
1117
|
+
}
|
|
1118
|
+
return ExecutionGate.check(this._spec, {
|
|
1119
|
+
iteration: this._iteration,
|
|
1120
|
+
config: { maxIterations: this._config.maxIterations },
|
|
1121
|
+
isBlockedByFailures: () => this.isBlockedByFailures()
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Step 2: Context assembly
|
|
1126
|
+
*/
|
|
1127
|
+
_assembleContext() {
|
|
1128
|
+
if (!this._contract) {
|
|
1129
|
+
throw new Error("Cannot assemble context without contract");
|
|
1130
|
+
}
|
|
1131
|
+
const nextPending = this._state.getNextPendingCriterion();
|
|
1132
|
+
if (nextPending) {
|
|
1133
|
+
this._state.setCriterionStatus(nextPending, "in_progress");
|
|
1134
|
+
}
|
|
1135
|
+
const focus = nextPending ? createWorkingFocus(
|
|
1136
|
+
nextPending,
|
|
1137
|
+
this._getCriterionText(nextPending),
|
|
1138
|
+
this._state.getLastToolCall()?.files?.[0]
|
|
1139
|
+
) : void 0;
|
|
1140
|
+
return ContextAssembler.assemble(
|
|
1141
|
+
{
|
|
1142
|
+
contract: this._contract,
|
|
1143
|
+
iteration: this._iteration,
|
|
1144
|
+
progress: this._state.getProgress(),
|
|
1145
|
+
touchedFiles: this._state.getTouchedFiles(),
|
|
1146
|
+
criteriaStatus: this._state.getAllCriteriaStatus(),
|
|
1147
|
+
failureSummary: this._failures.summarize(),
|
|
1148
|
+
currentFocus: focus,
|
|
1149
|
+
lastToolCall: this._state.getLastToolCall()
|
|
1150
|
+
},
|
|
1151
|
+
this._config.contextAssembler
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Step 3: Executor execution
|
|
1156
|
+
*/
|
|
1157
|
+
async _execute(context) {
|
|
1158
|
+
return await this._config.executor.execute(context);
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Step 4: Record result
|
|
1162
|
+
*/
|
|
1163
|
+
_recordResult(result) {
|
|
1164
|
+
const summary = createToolCallSummary(
|
|
1165
|
+
result.tool ?? "unknown",
|
|
1166
|
+
result.success,
|
|
1167
|
+
result.filesModified,
|
|
1168
|
+
result.error ? String(result.error) : void 0
|
|
1169
|
+
);
|
|
1170
|
+
this._state.recordToolCall(summary);
|
|
1171
|
+
if (result.filesModified) {
|
|
1172
|
+
this._state.touchFiles(result.filesModified);
|
|
1173
|
+
}
|
|
1174
|
+
if (!result.success) {
|
|
1175
|
+
const pattern = result.tool ? PatternExtractor.fromToolOutput(result.tool, result.error) : null;
|
|
1176
|
+
if (pattern && result.tool) {
|
|
1177
|
+
const currentCriterion = this._state.getCriteriaByStatus("in_progress")[0] ?? this._state.getCriteriaByStatus("failed")[0] ?? "unknown";
|
|
1178
|
+
this._failures.record({
|
|
1179
|
+
criterionId: currentCriterion,
|
|
1180
|
+
pattern,
|
|
1181
|
+
hint: PatternExtractor.extractHint(result.error)
|
|
1182
|
+
});
|
|
1183
|
+
if (currentCriterion !== "unknown") {
|
|
1184
|
+
const inProgress = this._state.getCriteriaByStatus("in_progress")[0];
|
|
1185
|
+
if (inProgress) {
|
|
1186
|
+
this._state.setCriterionStatus(inProgress, "pending");
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
} else {
|
|
1191
|
+
const currentCriterion = this._state.getCriteriaByStatus("in_progress")[0];
|
|
1192
|
+
if (currentCriterion) {
|
|
1193
|
+
const verificationMode = this._contract?.completion.verification.mode ?? "none";
|
|
1194
|
+
const finalStatus = verificationMode === "none" ? "verified" : "satisfied";
|
|
1195
|
+
this._state.setCriterionStatus(currentCriterion, finalStatus);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Step 5: Check completion
|
|
1201
|
+
*/
|
|
1202
|
+
_checkCompletion() {
|
|
1203
|
+
if (!this._spec) {
|
|
1204
|
+
return createRecommendation(
|
|
1205
|
+
"",
|
|
1206
|
+
false,
|
|
1207
|
+
"low",
|
|
1208
|
+
createEvidence(/* @__PURE__ */ new Map(), []),
|
|
1209
|
+
"abort",
|
|
1210
|
+
"No spec provided",
|
|
1211
|
+
false
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
const evidence = this._buildEvidence();
|
|
1215
|
+
const progress = this._state.getProgress();
|
|
1216
|
+
const verificationMode = this._contract?.completion.verification.mode ?? "none";
|
|
1217
|
+
return CompletionDetector.evaluate(this._spec, progress, evidence, verificationMode);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Build completion evidence from current state
|
|
1221
|
+
*/
|
|
1222
|
+
_buildEvidence() {
|
|
1223
|
+
return createEvidence(
|
|
1224
|
+
this._state.getAllCriteriaStatus(),
|
|
1225
|
+
this._state.getTouchedFiles()
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Get criterion text by ID
|
|
1230
|
+
*/
|
|
1231
|
+
_getCriterionText(criterionId) {
|
|
1232
|
+
const criterion = this._contract?.completion.criteria.find((c) => c.id === criterionId);
|
|
1233
|
+
return criterion?.description ?? criterionId;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Map gate reason to session status
|
|
1237
|
+
*/
|
|
1238
|
+
_mapGateReasonToStatus(reason) {
|
|
1239
|
+
switch (reason) {
|
|
1240
|
+
case "spec_not_found":
|
|
1241
|
+
case "spec_cancelled":
|
|
1242
|
+
case "spec_already_implemented":
|
|
1243
|
+
return "blocked";
|
|
1244
|
+
case "max_iterations_exceeded":
|
|
1245
|
+
case "blocked_by_failures":
|
|
1246
|
+
return "failed";
|
|
1247
|
+
default:
|
|
1248
|
+
return "blocked";
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
function createSession(spec, config) {
|
|
1253
|
+
return new ExecutionSession(spec, config);
|
|
1254
|
+
}
|
|
1255
|
+
export {
|
|
1256
|
+
CompletionDetector,
|
|
1257
|
+
ContextAssembler,
|
|
1258
|
+
ContractFactory,
|
|
1259
|
+
DEFAULT_ASSEMBLE_OPTIONS,
|
|
1260
|
+
DEFAULT_FAILURE_CONFIG,
|
|
1261
|
+
EMPTY_BOUNDARIES,
|
|
1262
|
+
EMPTY_COMPLETION,
|
|
1263
|
+
EMPTY_SNAPSHOT,
|
|
1264
|
+
ExecutionGate,
|
|
1265
|
+
ExecutionSession,
|
|
1266
|
+
ExecutionState,
|
|
1267
|
+
FailureMemory,
|
|
1268
|
+
PatternExtractor,
|
|
1269
|
+
RalphAdapter,
|
|
1270
|
+
computeProgress,
|
|
1271
|
+
createEvidence,
|
|
1272
|
+
createFailureResult,
|
|
1273
|
+
createProposal,
|
|
1274
|
+
createRalphAdapter,
|
|
1275
|
+
createRecommendation,
|
|
1276
|
+
createSession,
|
|
1277
|
+
createSuccessResult,
|
|
1278
|
+
createToolCallSummary,
|
|
1279
|
+
createWorkingFocus,
|
|
1280
|
+
isExecutorAvailable,
|
|
1281
|
+
restoreFromSnapshot
|
|
1282
|
+
};
|