loopforge 1.3.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/dist/backends/fs.d.ts +5 -0
- package/dist/backends/fs.d.ts.map +1 -1
- package/dist/backends/fs.js +67 -16
- package/dist/backends/fs.js.map +1 -1
- package/dist/builder.d.ts +0 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +14 -25
- package/dist/builder.js.map +1 -1
- package/dist/engine.d.ts +14 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +206 -105
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +4 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +5 -2
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +533 -19
- package/dist/loop-compiler.js.map +1 -1
- package/dist/mcp/session.d.ts +21 -2
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +257 -16
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +153 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +162 -6
- package/dist/mcp/tools.js.map +1 -1
- package/dist/observability.d.ts +23 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +29 -0
- package/dist/observability.js.map +1 -0
- package/dist/policy.d.ts +15 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +8 -0
- package/dist/policy.js.map +1 -1
- package/dist/protocol.d.ts +114 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +44 -3
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +51 -3
- package/dist/runtime.js.map +1 -1
- package/dist/tests/builder.test.js +1 -25
- package/dist/tests/builder.test.js.map +1 -1
- package/dist/tests/engine.test.js +213 -38
- package/dist/tests/engine.test.js.map +1 -1
- package/dist/tests/generate-schema.test.js +4 -4
- package/dist/tests/generate-schema.test.js.map +1 -1
- package/dist/tests/loop-compiler.test.js +212 -2
- package/dist/tests/loop-compiler.test.js.map +1 -1
- package/dist/tests/mcp.test.js +273 -0
- package/dist/tests/mcp.test.js.map +1 -1
- package/dist/tests/protocol.test.js +2 -4
- package/dist/tests/protocol.test.js.map +1 -1
- package/dist/tests/verification-gate.test.d.ts +2 -0
- package/dist/tests/verification-gate.test.d.ts.map +1 -0
- package/dist/tests/verification-gate.test.js +407 -0
- package/dist/tests/verification-gate.test.js.map +1 -0
- package/dist/verification-gate.d.ts +26 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +232 -0
- package/dist/verification-gate.js.map +1 -0
- package/loop_policy.json +8 -0
- package/package.json +1 -2
- package/skills/perception/SKILL.md +102 -53
- package/dist/autonomous.d.ts +0 -84
- package/dist/autonomous.d.ts.map +0 -1
- package/dist/autonomous.js +0 -250
- package/dist/autonomous.js.map +0 -1
- package/dist/backends/ai.d.ts +0 -37
- package/dist/backends/ai.d.ts.map +0 -1
- package/dist/backends/ai.js +0 -98
- package/dist/backends/ai.js.map +0 -1
- package/dist/cli.d.ts +0 -7
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -422
- package/dist/cli.js.map +0 -1
- package/dist/tests/adapter.test.js +0 -104
- package/dist/tests/adapter.test.js.map +0 -1
- package/dist/tests/autonomous.test.d.ts +0 -7
- package/dist/tests/autonomous.test.d.ts.map +0 -1
- package/dist/tests/autonomous.test.js +0 -294
- package/dist/tests/autonomous.test.js.map +0 -1
package/dist/loop-compiler.js
CHANGED
|
@@ -237,9 +237,140 @@ function computeConstraintRetirement(activeConstraints, loopId, currentRound, va
|
|
|
237
237
|
return { active: pruned, retired };
|
|
238
238
|
}
|
|
239
239
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
240
|
+
// P4: Progress evidence helpers
|
|
241
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
242
|
+
/** Extract execution evidence from a previous round in the vault.
|
|
243
|
+
* Used for progress gradient calculation and trend detection. */
|
|
244
|
+
function getPreviousRoundEvidence(loopId, roundNum, vaultContext) {
|
|
245
|
+
if (vaultContext === null)
|
|
246
|
+
return null;
|
|
247
|
+
const results = vaultContext.results || [];
|
|
248
|
+
for (const r of results) {
|
|
249
|
+
const lineage = (r.loop_lineage || r.lineage || {});
|
|
250
|
+
if (lineage.loop_id !== loopId)
|
|
251
|
+
continue;
|
|
252
|
+
if (lineage.round !== roundNum)
|
|
253
|
+
continue;
|
|
254
|
+
const evidence = (r.execution_evidence ?? lineage.execution_evidence);
|
|
255
|
+
if (evidence) {
|
|
256
|
+
return {
|
|
257
|
+
progress_estimate: evidence.progress_estimate ?? 0,
|
|
258
|
+
success_criteria_met: evidence.success_criteria_met ?? [],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
/** Count consecutive rounds (including current) where progress delta
|
|
266
|
+
* has been below the stall threshold. */
|
|
267
|
+
function countProgressStallRounds(loopId, currentRound, vaultContext, threshold) {
|
|
268
|
+
if (vaultContext === null || currentRound < 2)
|
|
269
|
+
return 0;
|
|
270
|
+
let count = 0;
|
|
271
|
+
for (let r = currentRound; r >= 2; r--) {
|
|
272
|
+
const curr = getPreviousRoundEvidence(loopId, r, vaultContext);
|
|
273
|
+
const prev = getPreviousRoundEvidence(loopId, r - 1, vaultContext);
|
|
274
|
+
if (curr && prev) {
|
|
275
|
+
const delta = curr.progress_estimate - prev.progress_estimate;
|
|
276
|
+
if (delta < threshold && curr.progress_estimate < 0.95) {
|
|
277
|
+
count++;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return count;
|
|
288
|
+
}
|
|
289
|
+
/** Detect repeated failure patterns in sampled rounds.
|
|
290
|
+
* A failure pattern = 2+ consecutive rounds where:
|
|
291
|
+
* 1. quality_score < 3 (failed or weak)
|
|
292
|
+
* 2. same technique was used
|
|
293
|
+
* 3. task text is similar (Jaccard > 0.4)
|
|
294
|
+
*
|
|
295
|
+
* Returns human-readable descriptions for injection into the prompt. */
|
|
296
|
+
function detectFailurePatterns(sampled) {
|
|
297
|
+
if (sampled.length < 2)
|
|
298
|
+
return [];
|
|
299
|
+
const patterns = [];
|
|
300
|
+
let runStart = -1;
|
|
301
|
+
for (let i = 0; i < sampled.length; i++) {
|
|
302
|
+
const r = sampled[i];
|
|
303
|
+
const isFailed = r.quality_score < 3;
|
|
304
|
+
if (isFailed && runStart === -1) {
|
|
305
|
+
runStart = i;
|
|
306
|
+
}
|
|
307
|
+
else if (!isFailed && runStart !== -1) {
|
|
308
|
+
if (i - runStart >= 2) {
|
|
309
|
+
const pattern = classifyRun(sampled.slice(runStart, i));
|
|
310
|
+
if (pattern)
|
|
311
|
+
patterns.push(pattern);
|
|
312
|
+
}
|
|
313
|
+
runStart = -1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Handle run at end of array
|
|
317
|
+
if (runStart !== -1 && sampled.length - runStart >= 2) {
|
|
318
|
+
const pattern = classifyRun(sampled.slice(runStart));
|
|
319
|
+
if (pattern)
|
|
320
|
+
patterns.push(pattern);
|
|
321
|
+
}
|
|
322
|
+
return patterns;
|
|
323
|
+
}
|
|
324
|
+
/** Classify a run of consecutive low-quality rounds as a failure pattern.
|
|
325
|
+
* Returns null if the rounds are too dissimilar to be the same pattern. */
|
|
326
|
+
function classifyRun(run) {
|
|
327
|
+
if (run.length < 2)
|
|
328
|
+
return null;
|
|
329
|
+
// Must all use the same technique
|
|
330
|
+
const technique = run[0].technique_used || "unknown";
|
|
331
|
+
if (!run.every((r) => (r.technique_used || "unknown") === technique)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
// Task text must be similar across the run (pairwise Jaccard)
|
|
335
|
+
const tokensList = run.map((r) => tokenize(r.task.toLowerCase()));
|
|
336
|
+
for (let i = 0; i < tokensList.length - 1; i++) {
|
|
337
|
+
for (let j = i + 1; j < tokensList.length; j++) {
|
|
338
|
+
if (jaccard(tokensList[i], tokensList[j]) < 0.4)
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const rounds = `R${run[0].round}-R${run[run.length - 1].round}`;
|
|
343
|
+
const taskPreview = run[0].task.slice(0, 80);
|
|
344
|
+
const avgScore = (run.reduce((s, r) => s + r.quality_score, 0) / run.length).toFixed(1);
|
|
345
|
+
return (`technique '${technique}' on task '${taskPreview}' ` +
|
|
346
|
+
`failed ${run.length} consecutive rounds (${rounds}, avg score ${avgScore}) — ` +
|
|
347
|
+
`consider strategy change`);
|
|
348
|
+
}
|
|
349
|
+
/** Extract round numbers referenced in failure pattern descriptions.
|
|
350
|
+
* Used by buildRollingSummary to identify which rounds to demote. */
|
|
351
|
+
function extractFailureRoundNums(patterns) {
|
|
352
|
+
const nums = new Set();
|
|
353
|
+
for (const p of patterns) {
|
|
354
|
+
// Match "R2-R4" or "R5"
|
|
355
|
+
const rangeMatch = p.match(/R(\d+)-R(\d+)/);
|
|
356
|
+
if (rangeMatch) {
|
|
357
|
+
const start = parseInt(rangeMatch[1], 10);
|
|
358
|
+
const end = parseInt(rangeMatch[2], 10);
|
|
359
|
+
for (let r = start; r <= end; r++)
|
|
360
|
+
nums.add(r);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
const singleMatch = p.match(/R(\d+)/);
|
|
364
|
+
if (singleMatch)
|
|
365
|
+
nums.add(parseInt(singleMatch[1], 10));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return nums;
|
|
369
|
+
}
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
240
371
|
// Rolling Summary (v3.5)
|
|
241
372
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
242
|
-
function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
373
|
+
export function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
243
374
|
if (vaultContext === null)
|
|
244
375
|
return null;
|
|
245
376
|
const results = vaultContext.results || [];
|
|
@@ -310,7 +441,7 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
|
310
441
|
}
|
|
311
442
|
}
|
|
312
443
|
}
|
|
313
|
-
|
|
444
|
+
let recurringIssues = [...violationCounts.entries()]
|
|
314
445
|
.filter(([, count]) => count >= 2)
|
|
315
446
|
.sort((a, b) => b[1] - a[1])
|
|
316
447
|
.map(([v, count]) => `${v} (appeared in ${count} rounds)`);
|
|
@@ -321,6 +452,57 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
|
321
452
|
keyLessons.push(`[R${r.round}] ${String(r.output_summary).slice(0, 200)}`);
|
|
322
453
|
}
|
|
323
454
|
}
|
|
455
|
+
// v1.7: Failure lineage weighting — detect and demote failure patterns
|
|
456
|
+
const sampledRounds = sampled.map((r) => ({
|
|
457
|
+
round: r.round,
|
|
458
|
+
quality_score: r.quality_score,
|
|
459
|
+
task: r.task,
|
|
460
|
+
output_summary: r.output_summary,
|
|
461
|
+
constraint_violations: r.constraint_violations,
|
|
462
|
+
technique_used: r.technique_used,
|
|
463
|
+
}));
|
|
464
|
+
const failurePatterns = detectFailurePatterns(sampledRounds);
|
|
465
|
+
const failedRoundNums = extractFailureRoundNums(failurePatterns);
|
|
466
|
+
// Re-weight key_lessons: lessons from failure-pattern rounds go to end
|
|
467
|
+
if (failedRoundNums.size > 0 && keyLessons.length > 0) {
|
|
468
|
+
const promoted = [];
|
|
469
|
+
const demoted = [];
|
|
470
|
+
for (const kl of keyLessons) {
|
|
471
|
+
const rndMatch = kl.match(/^\[R(\d+)\]/);
|
|
472
|
+
if (rndMatch && failedRoundNums.has(parseInt(rndMatch[1], 10))) {
|
|
473
|
+
demoted.push(kl.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
promoted.push(kl);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
keyLessons.length = 0;
|
|
480
|
+
keyLessons.push(...promoted, ...demoted);
|
|
481
|
+
}
|
|
482
|
+
// Re-weight recurring_issues: mark issues that ONLY appear in failure-pattern rounds
|
|
483
|
+
if (failedRoundNums.size > 0 && recurringIssues.length > 0) {
|
|
484
|
+
// Build per-round violation sets
|
|
485
|
+
const failureViolations = new Set();
|
|
486
|
+
const successViolations = new Set();
|
|
487
|
+
for (const r of sampledRounds) {
|
|
488
|
+
const target = failedRoundNums.has(r.round) ? failureViolations : successViolations;
|
|
489
|
+
for (const v of r.constraint_violations) {
|
|
490
|
+
target.add(String(v).trim().toLowerCase());
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
recurringIssues.length = 0;
|
|
494
|
+
recurringIssues.push(...[...violationCounts.entries()]
|
|
495
|
+
.filter(([, count]) => count >= 2)
|
|
496
|
+
.sort((a, b) => b[1] - a[1])
|
|
497
|
+
.map(([v, count]) => {
|
|
498
|
+
const vNorm = v.trim().toLowerCase();
|
|
499
|
+
// Mark if this issue only appears in failure rounds (possible dead end)
|
|
500
|
+
if (failureViolations.has(vNorm) && !successViolations.has(vNorm)) {
|
|
501
|
+
return `${v} (appeared in ${count} rounds) ⚠️ [Possible dead end — only in failed rounds]`;
|
|
502
|
+
}
|
|
503
|
+
return `${v} (appeared in ${count} rounds)`;
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
324
506
|
return makeRollingSummary({
|
|
325
507
|
quality_trajectory: trajectory,
|
|
326
508
|
trajectory_direction: direction,
|
|
@@ -329,9 +511,10 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
|
|
|
329
511
|
key_lessons: keyLessons,
|
|
330
512
|
rounds_sampled: sampled.length,
|
|
331
513
|
generated_at_round: currentRound,
|
|
514
|
+
failed_patterns: failurePatterns.length ? failurePatterns : [],
|
|
332
515
|
});
|
|
333
516
|
}
|
|
334
|
-
function formatRollingSummaryForPrompt(rs) {
|
|
517
|
+
export function formatRollingSummaryForPrompt(rs) {
|
|
335
518
|
if (rs === null || rs.rounds_sampled === 0)
|
|
336
519
|
return "";
|
|
337
520
|
const lines = [
|
|
@@ -353,6 +536,18 @@ function formatRollingSummaryForPrompt(rs) {
|
|
|
353
536
|
lines.push(`- ⚠️ ${ri}`);
|
|
354
537
|
lines.push("");
|
|
355
538
|
}
|
|
539
|
+
// v1.7: Failure patterns — explicit warnings about repeated failure paths
|
|
540
|
+
if (rs.failed_patterns && rs.failed_patterns.length) {
|
|
541
|
+
lines.push("### ⚠️ Failure Patterns — Consider Strategy Change");
|
|
542
|
+
lines.push("");
|
|
543
|
+
lines.push("The following approaches failed repeatedly. The compiler has demoted " +
|
|
544
|
+
"their influence on key lessons. Consider a different technique or " +
|
|
545
|
+
"narrower scope.");
|
|
546
|
+
for (const fp of rs.failed_patterns) {
|
|
547
|
+
lines.push(`- 🚫 ${fp}`);
|
|
548
|
+
}
|
|
549
|
+
lines.push("");
|
|
550
|
+
}
|
|
356
551
|
if (rs.key_lessons.length) {
|
|
357
552
|
lines.push("**Key Lessons From High-Score Rounds**:");
|
|
358
553
|
for (const kl of rs.key_lessons)
|
|
@@ -384,7 +579,23 @@ export function decideLevel(request, vaultContext) {
|
|
|
384
579
|
// Gate 3: goal_id stability
|
|
385
580
|
if (goalId !== prev.goal_id)
|
|
386
581
|
return "l2";
|
|
387
|
-
// Gate 4:
|
|
582
|
+
// Gate 4: P0–P5 cognitive evolution signals → full recompile (v1.7)
|
|
583
|
+
// Check BEFORE the simple repair/new-constraint gates because evolution signals
|
|
584
|
+
// (constraint discovery, objective refinement, self-correction) need full L2
|
|
585
|
+
// processing. Only structural evolution signals trigger L2 — execution_evidence
|
|
586
|
+
// alone does not (it is processed via the rolling summary at L1).
|
|
587
|
+
if (request.last_round_result) {
|
|
588
|
+
const lr = request.last_round_result;
|
|
589
|
+
if ((lr.discovered_constraints?.length ?? 0) > 0 ||
|
|
590
|
+
(lr.objective_refinement?.trim().length ?? 0) > 0 ||
|
|
591
|
+
(lr.emerged_subtasks?.length ?? 0) > 0 ||
|
|
592
|
+
(lr.retracted_constraints?.length ?? 0) > 0 ||
|
|
593
|
+
(lr.revised_success_criteria?.length ?? 0) > 0 ||
|
|
594
|
+
(lr.wrong_assumptions?.length ?? 0) > 0) {
|
|
595
|
+
return "l2";
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Gate 5: Explicit failures or new constraints → patch
|
|
388
599
|
const hasNewConstraints = request.constraints_from_plan.length > 0;
|
|
389
600
|
const hasNewFailures = request.last_round_result !== null &&
|
|
390
601
|
!request.last_round_result.success;
|
|
@@ -601,7 +812,8 @@ function extractObjectiveFromPlan(planPath) {
|
|
|
601
812
|
const goalPatterns = ["goal", "objective", "目标", "目的", "意图"];
|
|
602
813
|
const successPatterns = [
|
|
603
814
|
"success criteria", "acceptance criteria", "验收标准", "成功标准",
|
|
604
|
-
"done when", "完成标准", "交付标准",
|
|
815
|
+
"done when", "完成标准", "交付标准", "outcome", "deliverable",
|
|
816
|
+
"结果", "产出", "交付物",
|
|
605
817
|
];
|
|
606
818
|
const constraintPatterns = [
|
|
607
819
|
"hard constraint", "constraint", "non-goal", "out of scope",
|
|
@@ -641,6 +853,12 @@ function extractObjectiveFromPlan(planPath) {
|
|
|
641
853
|
sections.goal.push(stripped);
|
|
642
854
|
}
|
|
643
855
|
}
|
|
856
|
+
else if (stripped && currentSection === "success" && stripped.length > 10) {
|
|
857
|
+
// P3: Also capture free-text paragraphs under success criteria headers
|
|
858
|
+
if (!/^#/.test(stripped)) {
|
|
859
|
+
sections.success.push(stripped);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
644
862
|
}
|
|
645
863
|
if (!Object.values(sections).some((v) => v.length))
|
|
646
864
|
return null;
|
|
@@ -656,16 +874,33 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
|
|
|
656
874
|
let objective = task.trim().slice(0, 200);
|
|
657
875
|
const successCriteria = [];
|
|
658
876
|
const hardConstraints = [...constraints];
|
|
877
|
+
// P3: Outcome-oriented default success criteria — describe the RESULT,
|
|
878
|
+
// not the process. The process emerges from the loop.
|
|
659
879
|
if (/test|测试/i.test(task)) {
|
|
660
|
-
successCriteria.push("All tests pass");
|
|
880
|
+
successCriteria.push("All tests pass with no regressions");
|
|
661
881
|
}
|
|
662
882
|
if (/compat|兼容/i.test(task)) {
|
|
663
|
-
successCriteria.push("
|
|
883
|
+
successCriteria.push("Existing API contracts and integrations unchanged");
|
|
664
884
|
}
|
|
665
885
|
if (/security|安全|audit/i.test(task)) {
|
|
666
|
-
successCriteria.push("
|
|
886
|
+
successCriteria.push("All identified vulnerabilities resolved or documented");
|
|
887
|
+
}
|
|
888
|
+
if (/fix|修复|bug/i.test(task)) {
|
|
889
|
+
successCriteria.push("Root cause addressed and verified, not just symptoms");
|
|
890
|
+
}
|
|
891
|
+
if (/build|create|implement|开发|实现|构建/i.test(task)) {
|
|
892
|
+
successCriteria.push("Deliverable matches specification and passes review");
|
|
667
893
|
}
|
|
668
|
-
|
|
894
|
+
if (/refactor|重构/i.test(task)) {
|
|
895
|
+
successCriteria.push("Behavior preserved, structure improved, no new failures");
|
|
896
|
+
}
|
|
897
|
+
if (/migrate|迁移|upgrade|升级/i.test(task)) {
|
|
898
|
+
successCriteria.push("Migration completes without data loss or downtime");
|
|
899
|
+
}
|
|
900
|
+
if (/perf|performance|optimize|性能|优化/i.test(task)) {
|
|
901
|
+
successCriteria.push("Measurable improvement verified by benchmarks");
|
|
902
|
+
}
|
|
903
|
+
// P3: Try to extract richer objective from plan_source
|
|
669
904
|
if (request.plan_source) {
|
|
670
905
|
const planExtracted = extractObjectiveFromPlan(request.plan_source);
|
|
671
906
|
if (planExtracted) {
|
|
@@ -683,11 +918,27 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
|
|
|
683
918
|
hardConstraints.push(`Follow plan: ${request.plan_source}`);
|
|
684
919
|
}
|
|
685
920
|
}
|
|
921
|
+
// P3: Derive domain from task content for context-aware defaults
|
|
686
922
|
if (!successCriteria.length) {
|
|
687
|
-
|
|
923
|
+
if (/\.sol|solidity|contract|智能合约/i.test(task)) {
|
|
924
|
+
successCriteria.push("All functions respect check-effects-interactions pattern");
|
|
925
|
+
successCriteria.push("No unchecked external calls or reentrancy vectors");
|
|
926
|
+
}
|
|
927
|
+
else if (/\.tsx?|\.jsx?|react|component|组件/i.test(task)) {
|
|
928
|
+
successCriteria.push("Component renders correctly across all states (loading, empty, error)");
|
|
929
|
+
successCriteria.push("No prop-type or accessibility regressions");
|
|
930
|
+
}
|
|
931
|
+
else if (/api|endpoint|接口|route/i.test(task)) {
|
|
932
|
+
successCriteria.push("Endpoint responds correctly for all defined status codes");
|
|
933
|
+
successCriteria.push("Error responses follow the project's error schema");
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
successCriteria.push("Task goal achieved with verifiable evidence");
|
|
937
|
+
}
|
|
688
938
|
}
|
|
689
939
|
if (!hardConstraints.length) {
|
|
690
|
-
hardConstraints.push("Do not modify files outside scope");
|
|
940
|
+
hardConstraints.push("Do not modify files outside the stated scope");
|
|
941
|
+
hardConstraints.push("Preserve existing test coverage — no test deletions without replacement");
|
|
691
942
|
}
|
|
692
943
|
return makeLoopObjective({
|
|
693
944
|
objective,
|
|
@@ -695,6 +946,8 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
|
|
|
695
946
|
hard_constraints: hardConstraints,
|
|
696
947
|
created_at_round: 1,
|
|
697
948
|
loop_id: request.loop_id,
|
|
949
|
+
version: 1,
|
|
950
|
+
refinement_history: [],
|
|
698
951
|
});
|
|
699
952
|
}
|
|
700
953
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -825,8 +1078,19 @@ export function compileL2(request, vaultContext) {
|
|
|
825
1078
|
const referenceFile = analysis.reference_file;
|
|
826
1079
|
// v3.5: Rolling summary
|
|
827
1080
|
const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
|
|
1081
|
+
// P5c: Wrong assumptions — must be applied BEFORE formatting so they appear
|
|
1082
|
+
// in the rendered cross-round summary text injected into the prompt.
|
|
1083
|
+
const lr = request.last_round_result;
|
|
1084
|
+
if (lr?.wrong_assumptions?.length) {
|
|
1085
|
+
if (rollingSummary) {
|
|
1086
|
+
rollingSummary.key_lessons = [
|
|
1087
|
+
...rollingSummary.key_lessons,
|
|
1088
|
+
...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
|
|
1089
|
+
];
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
828
1092
|
const rollingText = formatRollingSummaryForPrompt(rollingSummary);
|
|
829
|
-
//
|
|
1093
|
+
// ── Loop Objective: create at round 1, load from vault at later rounds ──
|
|
830
1094
|
let loopObjective = null;
|
|
831
1095
|
if (request.round === 1) {
|
|
832
1096
|
if (request.loop_objective) {
|
|
@@ -836,9 +1100,113 @@ export function compileL2(request, vaultContext) {
|
|
|
836
1100
|
loopObjective = computeLoopObjectiveFromTask(request, vaultContext);
|
|
837
1101
|
}
|
|
838
1102
|
}
|
|
1103
|
+
else {
|
|
1104
|
+
// P1 fix: Load loop objective from vault for round > 1
|
|
1105
|
+
const vaultLo = vaultGetLoopObjective(request.loop_id, vaultContext);
|
|
1106
|
+
if (vaultLo) {
|
|
1107
|
+
const loData = vaultLo.loop_objective;
|
|
1108
|
+
if (loData) {
|
|
1109
|
+
loopObjective = makeLoopObjective({
|
|
1110
|
+
objective: loData.objective ?? "",
|
|
1111
|
+
success_criteria: loData.success_criteria ?? [],
|
|
1112
|
+
hard_constraints: loData.hard_constraints ?? [],
|
|
1113
|
+
created_at_round: loData.created_at_round ?? 1,
|
|
1114
|
+
loop_id: loData.loop_id ?? request.loop_id,
|
|
1115
|
+
version: loData.version ?? 1,
|
|
1116
|
+
refinement_history: loData.refinement_history ?? [],
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (!loopObjective && request.loop_objective) {
|
|
1121
|
+
loopObjective = request.loop_objective;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// ── P1: Apply objective_refinement from last round ──
|
|
1125
|
+
if (loopObjective && request.last_round_result?.objective_refinement) {
|
|
1126
|
+
const refinement = request.last_round_result.objective_refinement.trim();
|
|
1127
|
+
if (refinement) {
|
|
1128
|
+
const version = (loopObjective.version ?? 1) + 1;
|
|
1129
|
+
const history = [...(loopObjective.refinement_history ?? []), refinement];
|
|
1130
|
+
// Enforce max versions from policy
|
|
1131
|
+
const policy = getPolicy();
|
|
1132
|
+
const maxVersions = policy.evolution?.max_objective_versions ?? 10;
|
|
1133
|
+
const prunedHistory = history.slice(-maxVersions);
|
|
1134
|
+
const refinedObjective = `${loopObjective.objective} [R${request.round}: ${refinement}]`;
|
|
1135
|
+
loopObjective = makeLoopObjective({
|
|
1136
|
+
...loopObjective,
|
|
1137
|
+
objective: refinedObjective,
|
|
1138
|
+
version,
|
|
1139
|
+
refinement_history: prunedHistory,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
// ── Build active constraints (base + LO + P0 discovered) ──
|
|
839
1144
|
let constraints = [...request.constraints_from_plan];
|
|
840
1145
|
if (loopObjective) {
|
|
841
|
-
constraints = [
|
|
1146
|
+
constraints = [
|
|
1147
|
+
...new Set([
|
|
1148
|
+
...constraints,
|
|
1149
|
+
...loopObjective.hard_constraints,
|
|
1150
|
+
...loopObjective.success_criteria,
|
|
1151
|
+
]),
|
|
1152
|
+
];
|
|
1153
|
+
}
|
|
1154
|
+
// P0: Merge discovered constraints from last round
|
|
1155
|
+
if (request.last_round_result?.discovered_constraints?.length) {
|
|
1156
|
+
const policy = getPolicy();
|
|
1157
|
+
const maxPerRound = policy.evolution?.max_discovered_constraints_per_round ?? 5;
|
|
1158
|
+
const maxActive = policy.evolution?.max_active_constraints ?? 15;
|
|
1159
|
+
const newConstraints = request.last_round_result.discovered_constraints
|
|
1160
|
+
.slice(0, maxPerRound)
|
|
1161
|
+
.filter((c) => !constraints.includes(c));
|
|
1162
|
+
constraints = [...constraints, ...newConstraints].slice(0, maxActive);
|
|
1163
|
+
}
|
|
1164
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1165
|
+
// P5: Self-correction — MUST process retractions/revisions BEFORE prompt build
|
|
1166
|
+
// so the technician compiler sees the corrected constraints and loop objective.
|
|
1167
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1168
|
+
const warnings = [];
|
|
1169
|
+
const retired = [];
|
|
1170
|
+
// P5a: Retracted constraints — remove from active set
|
|
1171
|
+
if (lr?.retracted_constraints?.length) {
|
|
1172
|
+
for (const rc of lr.retracted_constraints) {
|
|
1173
|
+
const idx = constraints.indexOf(rc);
|
|
1174
|
+
if (idx >= 0) {
|
|
1175
|
+
constraints.splice(idx, 1);
|
|
1176
|
+
retired.push(`[agent-retracted] ${rc}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
// P5b: Revised success criteria — update Loop Objective
|
|
1181
|
+
if (lr?.revised_success_criteria?.length && loopObjective) {
|
|
1182
|
+
const sc = [...loopObjective.success_criteria];
|
|
1183
|
+
for (const rev of lr.revised_success_criteria) {
|
|
1184
|
+
const idx = sc.indexOf(rev.old);
|
|
1185
|
+
if (idx >= 0) {
|
|
1186
|
+
sc[idx] = rev.new;
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
sc.push(rev.new);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const version = (loopObjective.version ?? 1) + 1;
|
|
1193
|
+
const history = [
|
|
1194
|
+
...(loopObjective.refinement_history ?? []),
|
|
1195
|
+
`R${request.round}: revised ${lr.revised_success_criteria.length} success criteria`,
|
|
1196
|
+
];
|
|
1197
|
+
const updatedLo = makeLoopObjective({
|
|
1198
|
+
...loopObjective,
|
|
1199
|
+
success_criteria: sc,
|
|
1200
|
+
version,
|
|
1201
|
+
refinement_history: history,
|
|
1202
|
+
});
|
|
1203
|
+
// Rebuild constraints to include revised criteria
|
|
1204
|
+
constraints = [...new Set([
|
|
1205
|
+
...constraints.filter((c) => !updatedLo.hard_constraints.includes(c) && !updatedLo.success_criteria.includes(c)),
|
|
1206
|
+
...updatedLo.hard_constraints,
|
|
1207
|
+
...updatedLo.success_criteria,
|
|
1208
|
+
])];
|
|
1209
|
+
loopObjective = updatedLo;
|
|
842
1210
|
}
|
|
843
1211
|
// ── Route to technique-specific specialist (v1.1 deep integration) ──
|
|
844
1212
|
let prompt;
|
|
@@ -854,16 +1222,126 @@ export function compileL2(request, vaultContext) {
|
|
|
854
1222
|
else {
|
|
855
1223
|
prompt = compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique);
|
|
856
1224
|
}
|
|
1225
|
+
// ── P2: Emerged subtasks → suggested next task ──
|
|
1226
|
+
let suggestedNextTask = "";
|
|
1227
|
+
if (request.last_round_result?.emerged_subtasks?.length) {
|
|
1228
|
+
suggestedNextTask = request.last_round_result.emerged_subtasks
|
|
1229
|
+
.slice(0, 3)
|
|
1230
|
+
.join("; ");
|
|
1231
|
+
}
|
|
1232
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1233
|
+
// P4: Consistency validation — cross-check agent claims
|
|
1234
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1235
|
+
const evidence = lr?.execution_evidence;
|
|
1236
|
+
if (evidence) {
|
|
1237
|
+
// Check 1: Agent claims action but reports no files changed
|
|
1238
|
+
if (evidence.files_changed.length === 0 && lr.output_summary) {
|
|
1239
|
+
const actionWords = /\b(fix|modif|chang|updat|refactor|implement|add|remov|delet|rewrit|修补|修改|更改|更新|重构|实现|添加|删除|重写)\b/i;
|
|
1240
|
+
if (actionWords.test(lr.output_summary)) {
|
|
1241
|
+
warnings.push("P4: Agent output claims changes but execution_evidence.files_changed is empty — possible oversight");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
// Check 2: Agent reports success but tests failed
|
|
1245
|
+
if (evidence.test_results && evidence.test_results.failed > 0 && lr.success) {
|
|
1246
|
+
warnings.push(`P4: Agent reports success=true but ${evidence.test_results.failed} tests failed — claims may not match reality`);
|
|
1247
|
+
}
|
|
1248
|
+
// Check 3: Progress estimate mismatch with criteria tracking
|
|
1249
|
+
if (loopObjective && loopObjective.success_criteria.length > 0) {
|
|
1250
|
+
const metCount = evidence.success_criteria_met.length;
|
|
1251
|
+
const totalCount = loopObjective.success_criteria.length;
|
|
1252
|
+
const objectiveProgress = totalCount > 0 ? metCount / totalCount : 0;
|
|
1253
|
+
const subjectiveProgress = evidence.progress_estimate;
|
|
1254
|
+
const policy = getPolicy();
|
|
1255
|
+
const mismatchThreshold = policy.evolution?.progress_mismatch_threshold ?? 0.3;
|
|
1256
|
+
if (Math.abs(objectiveProgress - subjectiveProgress) > mismatchThreshold) {
|
|
1257
|
+
warnings.push(`P4: Progress estimate mismatch — agent estimates ${(subjectiveProgress * 100).toFixed(0)}% but ${metCount}/${totalCount} criteria met (${(objectiveProgress * 100).toFixed(0)}%)`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1262
|
+
// P4: Progress gradient — early stall detection
|
|
1263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1264
|
+
if (evidence && vaultContext) {
|
|
1265
|
+
const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
|
|
1266
|
+
if (prevEvidence) {
|
|
1267
|
+
const delta = evidence.progress_estimate - prevEvidence.progress_estimate;
|
|
1268
|
+
const policy = getPolicy();
|
|
1269
|
+
const stallThreshold = policy.evolution?.progress_stall_threshold ?? 0.05;
|
|
1270
|
+
const stallRounds = policy.evolution?.progress_stall_rounds ?? 2;
|
|
1271
|
+
if (delta < stallThreshold && evidence.progress_estimate < 0.95) {
|
|
1272
|
+
// Check how many consecutive rounds have been below threshold
|
|
1273
|
+
const stallCount = countProgressStallRounds(request.loop_id, request.round, vaultContext, stallThreshold);
|
|
1274
|
+
if (stallCount >= stallRounds) {
|
|
1275
|
+
warnings.push(`P4: Progress has stalled — <${(stallThreshold * 100).toFixed(0)}% improvement for ${stallCount} consecutive rounds. Current: ${(evidence.progress_estimate * 100).toFixed(0)}%`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1281
|
+
// P4: Build Progress Dashboard for prompt injection
|
|
1282
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1283
|
+
let progressDashboard = "";
|
|
1284
|
+
if (evidence && loopObjective) {
|
|
1285
|
+
const totalCriteria = loopObjective.success_criteria.length;
|
|
1286
|
+
const metCriteria = evidence.success_criteria_met.length;
|
|
1287
|
+
const remainingCriteria = evidence.success_criteria_remaining;
|
|
1288
|
+
const objProgress = totalCriteria > 0 ? metCriteria / totalCriteria : evidence.progress_estimate;
|
|
1289
|
+
const subjProgress = evidence.progress_estimate;
|
|
1290
|
+
const lines = [
|
|
1291
|
+
"",
|
|
1292
|
+
"### Progress Dashboard",
|
|
1293
|
+
"",
|
|
1294
|
+
`**Overall**: ${metCriteria}/${totalCriteria} criteria met (${(objProgress * 100).toFixed(0)}%)`,
|
|
1295
|
+
];
|
|
1296
|
+
if (Math.abs(objProgress - subjProgress) > 0.05) {
|
|
1297
|
+
lines.push(`**Agent estimate**: ${(subjProgress * 100).toFixed(0)}%`);
|
|
1298
|
+
}
|
|
1299
|
+
if (evidence.files_changed.length) {
|
|
1300
|
+
lines.push(`**Files changed**: ${evidence.files_changed.join(", ")}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (evidence.test_results) {
|
|
1303
|
+
const tr = evidence.test_results;
|
|
1304
|
+
lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped`);
|
|
1305
|
+
}
|
|
1306
|
+
if (remainingCriteria.length) {
|
|
1307
|
+
lines.push("**Remaining criteria**:");
|
|
1308
|
+
for (const rc of remainingCriteria.slice(0, 5)) {
|
|
1309
|
+
lines.push(`- ${rc}`);
|
|
1310
|
+
}
|
|
1311
|
+
if (remainingCriteria.length > 5) {
|
|
1312
|
+
lines.push(`- ... and ${remainingCriteria.length - 5} more`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
lines.push("");
|
|
1316
|
+
// Compute trend from previous round
|
|
1317
|
+
if (vaultContext) {
|
|
1318
|
+
const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
|
|
1319
|
+
if (prevEvidence) {
|
|
1320
|
+
const trend = objProgress - (prevEvidence.success_criteria_met.length / Math.max(totalCriteria, 1));
|
|
1321
|
+
if (trend > 0.03) {
|
|
1322
|
+
lines.push(`**Trend**: ↑ advancing (+${(trend * 100).toFixed(0)}% this round)`);
|
|
1323
|
+
}
|
|
1324
|
+
else if (trend > -0.03) {
|
|
1325
|
+
lines.push("**Trend**: → stable");
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
lines.push(`**Trend**: ↓ regressing (${(trend * 100).toFixed(0)}% this round)`);
|
|
1329
|
+
}
|
|
1330
|
+
lines.push("");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
progressDashboard = lines.join("\n");
|
|
1334
|
+
}
|
|
857
1335
|
return makeLoopCompileResponse({
|
|
858
1336
|
status: "ok",
|
|
859
|
-
prompt,
|
|
1337
|
+
prompt: prompt + progressDashboard,
|
|
860
1338
|
recompile_level: "l2",
|
|
861
1339
|
diff_from_previous: request.round === 1 || request.plan_source
|
|
862
1340
|
? "Full recompile — new goal or first call."
|
|
863
1341
|
: "Full recompile — goal_id changed or strategy collapse.",
|
|
864
1342
|
lineage: [`${request.loop_id}:r${request.round}`],
|
|
865
1343
|
constraints_active: constraints,
|
|
866
|
-
constraints_retired:
|
|
1344
|
+
constraints_retired: retired,
|
|
867
1345
|
technique_used: technique,
|
|
868
1346
|
reference_file: referenceFile,
|
|
869
1347
|
rolling_summary: rollingSummary,
|
|
@@ -873,7 +1351,8 @@ export function compileL2(request, vaultContext) {
|
|
|
873
1351
|
goal_text_hash: computeGoalTextHash(request.task),
|
|
874
1352
|
loop_objective: loopObjective,
|
|
875
1353
|
plan_source: request.plan_source,
|
|
876
|
-
|
|
1354
|
+
suggested_next_task: suggestedNextTask,
|
|
1355
|
+
warnings,
|
|
877
1356
|
});
|
|
878
1357
|
}
|
|
879
1358
|
function buildHeader(ctx, technique) {
|
|
@@ -1207,7 +1686,8 @@ function compileToT(request, goalId, constraints, loopObjective, rollingText, an
|
|
|
1207
1686
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1208
1687
|
/** Build the standardized self-evaluation block appended to every compiled prompt.
|
|
1209
1688
|
* The agent MUST output a JSON self-evaluation between the delimiters.
|
|
1210
|
-
*
|
|
1689
|
+
* 4 required fields + 3 optional evolution fields (P0–P2) —
|
|
1690
|
+
* each consumed by at least one downstream function. */
|
|
1211
1691
|
export function buildSelfEvalBlock(round) {
|
|
1212
1692
|
return [
|
|
1213
1693
|
"",
|
|
@@ -1222,7 +1702,20 @@ export function buildSelfEvalBlock(round) {
|
|
|
1222
1702
|
' "success": true,',
|
|
1223
1703
|
` "output_summary": "<one paragraph — what was DONE in round ${round}, be specific>",`,
|
|
1224
1704
|
' "constraint_violations": [],',
|
|
1225
|
-
' "should_continue": true',
|
|
1705
|
+
' "should_continue": true,',
|
|
1706
|
+
' "discovered_constraints": [],',
|
|
1707
|
+
' "objective_refinement": "",',
|
|
1708
|
+
' "emerged_subtasks": [],',
|
|
1709
|
+
' "execution_evidence": {',
|
|
1710
|
+
' "files_changed": [],',
|
|
1711
|
+
' "test_results": {"passed": 0, "failed": 0, "skipped": 0},',
|
|
1712
|
+
' "success_criteria_met": [],',
|
|
1713
|
+
' "success_criteria_remaining": [],',
|
|
1714
|
+
' "progress_estimate": 0.0',
|
|
1715
|
+
' },',
|
|
1716
|
+
' "retracted_constraints": [],',
|
|
1717
|
+
' "revised_success_criteria": [],',
|
|
1718
|
+
' "wrong_assumptions": []',
|
|
1226
1719
|
"}",
|
|
1227
1720
|
"---end-loopforge-eval",
|
|
1228
1721
|
"```",
|
|
@@ -1238,6 +1731,23 @@ export function buildSelfEvalBlock(round) {
|
|
|
1238
1731
|
`- should_continue: false ONLY when the ENTIRE task is complete. ` +
|
|
1239
1732
|
`If there is more to audit/implement/test, say true. ` +
|
|
1240
1733
|
`This tells the autonomous runner when to stop.`,
|
|
1734
|
+
`- discovered_constraints (P0 — optional): New constraints you discovered this round. ` +
|
|
1735
|
+
`Omit or [] if none.`,
|
|
1736
|
+
`- objective_refinement (P1 — optional): Deepened understanding of the task goal. ` +
|
|
1737
|
+
`APPENDED to the original objective. Omit or "" if unchanged.`,
|
|
1738
|
+
`- emerged_subtasks (P2 — optional): Sub-problems that surfaced during execution. ` +
|
|
1739
|
+
`Omit or [] if none.`,
|
|
1740
|
+
`- execution_evidence (P4 — recommended): Structured record of what you actually did. ` +
|
|
1741
|
+
`files_changed: paths relative to project root. test_results: null if no tests run. ` +
|
|
1742
|
+
`success_criteria_met/remaining: track against the Loop Objective. ` +
|
|
1743
|
+
`progress_estimate: your honest estimate of overall completion (0.0 to 1.0). ` +
|
|
1744
|
+
`This enables real progress tracking and early stall detection.`,
|
|
1745
|
+
`- retracted_constraints (P5 — optional): Constraints you now believe are WRONG. ` +
|
|
1746
|
+
`Removed from active guardrails. Only retract if you have evidence. Omit or [] if none.`,
|
|
1747
|
+
`- revised_success_criteria (P5 — optional): Success criteria that need reformulation. ` +
|
|
1748
|
+
`Array of {old, new} objects. Applied to the Loop Objective. Omit or [] if none.`,
|
|
1749
|
+
`- wrong_assumptions (P5 — optional): Assumptions from earlier rounds that turned ` +
|
|
1750
|
+
`out to be incorrect. Recorded as key lessons. Omit or [] if none.`,
|
|
1241
1751
|
`- The JSON MUST appear between the ---loopforge-eval and ---end-loopforge-eval markers`,
|
|
1242
1752
|
`- Do NOT wrap the markers in code fences — output them as raw text`,
|
|
1243
1753
|
"",
|
|
@@ -1264,7 +1774,11 @@ export function compileLoop(request, vaultContext = null) {
|
|
|
1264
1774
|
const { warnings, suggestedNextTask, alignment, health } = computeAdvisories(request, vaultContext);
|
|
1265
1775
|
// Merge advisories into response
|
|
1266
1776
|
response.warnings = warnings;
|
|
1267
|
-
|
|
1777
|
+
// Only overwrite suggested_next_task from advisories if the compile function
|
|
1778
|
+
// didn't already set one (e.g. L2 sets it from emerged_subtasks)
|
|
1779
|
+
if (!response.suggested_next_task) {
|
|
1780
|
+
response.suggested_next_task = suggestedNextTask;
|
|
1781
|
+
}
|
|
1268
1782
|
response.task_alignment = alignment;
|
|
1269
1783
|
response.loop_health = health;
|
|
1270
1784
|
response.recompile_level = level;
|