loopforge 1.7.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +64 -3
  2. package/dist/backends/fs.d.ts.map +1 -1
  3. package/dist/backends/fs.js +1 -2
  4. package/dist/backends/fs.js.map +1 -1
  5. package/dist/backends/interface.d.ts +2 -0
  6. package/dist/backends/interface.d.ts.map +1 -1
  7. package/dist/builder.d.ts +6 -10
  8. package/dist/builder.d.ts.map +1 -1
  9. package/dist/builder.js +128 -63
  10. package/dist/builder.js.map +1 -1
  11. package/dist/engine.d.ts +16 -2
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +142 -54
  14. package/dist/engine.js.map +1 -1
  15. package/dist/index.d.ts +7 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/loop-compiler.d.ts +33 -2
  20. package/dist/loop-compiler.d.ts.map +1 -1
  21. package/dist/loop-compiler.js +391 -135
  22. package/dist/loop-compiler.js.map +1 -1
  23. package/dist/mcp/server.d.ts.map +1 -1
  24. package/dist/mcp/server.js +6 -4
  25. package/dist/mcp/server.js.map +1 -1
  26. package/dist/mcp/session.d.ts +18 -4
  27. package/dist/mcp/session.d.ts.map +1 -1
  28. package/dist/mcp/session.js +225 -24
  29. package/dist/mcp/session.js.map +1 -1
  30. package/dist/mcp/tools.d.ts +32 -1
  31. package/dist/mcp/tools.d.ts.map +1 -1
  32. package/dist/mcp/tools.js +33 -14
  33. package/dist/mcp/tools.js.map +1 -1
  34. package/dist/memory-bridge.d.ts +47 -0
  35. package/dist/memory-bridge.d.ts.map +1 -0
  36. package/dist/memory-bridge.js +290 -0
  37. package/dist/memory-bridge.js.map +1 -0
  38. package/dist/observability.d.ts +1 -1
  39. package/dist/observability.js +1 -1
  40. package/dist/policy.d.ts +66 -9
  41. package/dist/policy.d.ts.map +1 -1
  42. package/dist/policy.js +41 -28
  43. package/dist/policy.js.map +1 -1
  44. package/dist/protocol.d.ts +120 -16
  45. package/dist/protocol.d.ts.map +1 -1
  46. package/dist/protocol.js +23 -9
  47. package/dist/protocol.js.map +1 -1
  48. package/dist/replay.js +7 -7
  49. package/dist/replay.js.map +1 -1
  50. package/dist/runtime.d.ts +20 -3
  51. package/dist/runtime.d.ts.map +1 -1
  52. package/dist/runtime.js +420 -177
  53. package/dist/runtime.js.map +1 -1
  54. package/dist/tests/builder.test.js +40 -28
  55. package/dist/tests/builder.test.js.map +1 -1
  56. package/dist/tests/engine.test.js +13 -17
  57. package/dist/tests/engine.test.js.map +1 -1
  58. package/dist/tests/generate-schema.test.js +10 -7
  59. package/dist/tests/generate-schema.test.js.map +1 -1
  60. package/dist/tests/loop-compiler.test.js +481 -27
  61. package/dist/tests/loop-compiler.test.js.map +1 -1
  62. package/dist/tests/mcp.test.js +103 -103
  63. package/dist/tests/mcp.test.js.map +1 -1
  64. package/dist/tests/memory-bridge.test.d.ts +8 -0
  65. package/dist/tests/memory-bridge.test.d.ts.map +1 -0
  66. package/dist/tests/memory-bridge.test.js +383 -0
  67. package/dist/tests/memory-bridge.test.js.map +1 -0
  68. package/dist/tests/policy.test.js +5 -15
  69. package/dist/tests/policy.test.js.map +1 -1
  70. package/dist/tests/protocol.test.js +15 -3
  71. package/dist/tests/protocol.test.js.map +1 -1
  72. package/dist/tests/replay.test.js +9 -9
  73. package/dist/tests/replay.test.js.map +1 -1
  74. package/dist/tests/runtime.test.js +8 -8
  75. package/dist/tests/runtime.test.js.map +1 -1
  76. package/dist/tests/verification-gate.test.js +1 -1
  77. package/dist/tests/verification-gate.test.js.map +1 -1
  78. package/dist/verification-gate.d.ts +2 -2
  79. package/dist/verification-gate.js +2 -2
  80. package/loop_policy.json +28 -23
  81. package/package.json +1 -1
  82. package/skills/perception/SKILL.md +64 -12
  83. package/dist/adapter.d.ts +0 -17
  84. package/dist/adapter.d.ts.map +0 -1
  85. package/dist/adapter.js +0 -228
  86. package/dist/adapter.js.map +0 -1
  87. package/dist/tests/adapter.test.d.ts +0 -2
  88. package/dist/tests/adapter.test.d.ts.map +0 -1
package/dist/runtime.js CHANGED
@@ -14,13 +14,81 @@
14
14
  */
15
15
  import { EventEmitter } from "node:events";
16
16
  import { extractSelfEvaluation, heuristicSelfEvaluation, createEngine, } from "./engine.js";
17
+ import { tokenize, jaccard } from "./loop-compiler.js";
17
18
  import { Mode, RuntimeStatus, makeLoopCompileRequest, makeLoopRoundResult, makeSelfEvaluation, makeVaultConfig, } from "./protocol.js";
18
- import { getPolicy } from "./policy.js";
19
+ import { getPolicy, resolveAllowedPhases } from "./policy.js";
19
20
  import { verifySelfEvaluation } from "./verification-gate.js";
20
21
  import { logEvent } from "./observability.js";
22
+ import { tryAutoConfigure } from "./memory-bridge.js";
21
23
  // ═══════════════════════════════════════════════════════════════════════════
22
24
  // Helpers
23
25
  // ═══════════════════════════════════════════════════════════════════════════
26
+ /** Build a writeback payload from the loop's final state.
27
+ * Extracts project-level outcome, tactical lessons, and vault reference.
28
+ * Returns null if there is no meaningful data to write back. */
29
+ function buildWritebackPayload(loopId, task, stopReason, result) {
30
+ // Only write back for meaningful loops (at least 1 round completed)
31
+ if (result.roundsCompleted < 1)
32
+ return null;
33
+ const policy = getPolicy();
34
+ const wp = policy.memory_writeback;
35
+ const outcome = ["completed", "circuit_breaker", "stalled", "max_rounds", "stopped"].find((o) => stopReason === o) ?? "stopped";
36
+ // Build project entry
37
+ const projectEntry = {
38
+ title: `${task.slice(0, 80)} — ${outcome}`,
39
+ objective: task.slice(0, 200),
40
+ keyOutcome: stopReason === "task_complete"
41
+ ? `Completed successfully in ${result.roundsCompleted} rounds.`
42
+ : `Terminated with reason '${stopReason}' after ${result.roundsCompleted} rounds.`,
43
+ keyDiscoveries: [],
44
+ date: new Date().toISOString().split("T")[0],
45
+ };
46
+ // Build feedback entries from success trajectory patterns
47
+ const feedbackEntries = [];
48
+ const trajectory = result.successTrajectory;
49
+ if (trajectory.length >= 3) {
50
+ // Detect improving trend via success rate
51
+ const firstHalf = trajectory.slice(0, Math.floor(trajectory.length / 2));
52
+ const secondHalf = trajectory.slice(Math.floor(trajectory.length / 2));
53
+ const firstRate = firstHalf.filter(Boolean).length / firstHalf.length;
54
+ const secondRate = secondHalf.filter(Boolean).length / secondHalf.length;
55
+ if (secondRate - firstRate > 0.3) {
56
+ feedbackEntries.push({
57
+ rule: `Success rate improved from ${(firstRate * 100).toFixed(0)}% to ${(secondRate * 100).toFixed(0)}% over ${trajectory.length} rounds`,
58
+ why: `Mid-loop strategy adjustment unlocked progress in task "${task.slice(0, 80)}"`,
59
+ howToApply: "Consider the strategy used in later rounds for similar tasks",
60
+ });
61
+ }
62
+ }
63
+ // Detect flatlining — 3 consecutive failures
64
+ if (trajectory.length >= 3) {
65
+ const recent = trajectory.slice(-3);
66
+ if (recent.every((v) => v === false)) {
67
+ feedbackEntries.push({
68
+ rule: `Failed 3 consecutive rounds at end of loop`,
69
+ why: `Task "${task.slice(0, 80)}" stopped with reason '${stopReason}' — strategy may have stagnated`,
70
+ howToApply: "If re-attempting, start with a different technique than the one that produced the failures",
71
+ });
72
+ }
73
+ }
74
+ // Build reference entry pointing to vault
75
+ const referenceEntry = {
76
+ description: `LoopForge vault data for "${task.slice(0, 80)}"`,
77
+ vaultLocation: `.promptcraft/prompt_vault.json → loop:${loopId}:*`,
78
+ };
79
+ return {
80
+ loopId,
81
+ task,
82
+ outcome,
83
+ roundsCompleted: result.roundsCompleted,
84
+ projectEntry: {
85
+ ...projectEntry,
86
+ keyDiscoveries: projectEntry.keyDiscoveries.slice(0, wp.max_discoveries_in_project),
87
+ },
88
+ feedbackEntries: feedbackEntries.slice(0, wp.max_feedback_entries),
89
+ referenceEntry,
90
+ };
91
+ }
24
92
  function autoLoopId(task) {
25
93
  let slug = task.toLowerCase().trim().slice(0, 60);
26
94
  slug = slug.replace(/[^a-z0-9\s-]/g, "");
@@ -30,7 +98,7 @@ function autoLoopId(task) {
30
98
  function resolveConfig(raw) {
31
99
  const policy = getPolicy();
32
100
  const rtPolicy = policy.runtime;
33
- return {
101
+ const config = {
34
102
  task: raw.task,
35
103
  execute: raw.execute,
36
104
  loopId: raw.loopId || autoLoopId(raw.task),
@@ -50,7 +118,18 @@ function resolveConfig(raw) {
50
118
  onHeartbeat: raw.onHeartbeat,
51
119
  onTimeout: raw.onTimeout,
52
120
  onHealthWarning: raw.onHealthWarning,
121
+ memoryProvider: raw.memoryProvider,
122
+ memoryWriter: raw.memoryWriter,
53
123
  };
124
+ // Auto-detect claude-mem if no explicit provider was given
125
+ if (!config.memoryProvider && !config.memoryWriter) {
126
+ const auto = tryAutoConfigure();
127
+ if (auto.memoryProvider)
128
+ config.memoryProvider = auto.memoryProvider;
129
+ if (auto.memoryWriter)
130
+ config.memoryWriter = auto.memoryWriter;
131
+ }
132
+ return config;
54
133
  }
55
134
  // ═══════════════════════════════════════════════════════════════════════════
56
135
  // LoopRuntime
@@ -60,13 +139,20 @@ export class LoopRuntime extends EventEmitter {
60
139
  engine;
61
140
  _status = RuntimeStatus.IDLE;
62
141
  currentRound = 1;
63
- qualityTrajectory = [];
142
+ successTrajectory = [];
64
143
  heartbeatTimer = null;
65
144
  sigintHandler = null;
66
145
  sigtermHandler = null;
67
146
  // Verification gate state (v1.6 unified — was MCP-only, now also in runtime)
68
147
  lastSelfEval = null;
69
148
  pendingVerificationFlags = [];
149
+ // Memory integration state (v1.7)
150
+ injectionCount = 0;
151
+ lastInjectionRound = 0;
152
+ injectedContexts = [];
153
+ phase2Triggered = false;
154
+ phase3Triggered = false;
155
+ pendingExternalContext = "";
70
156
  // Shared mutable state between the main loop and the heartbeat tick
71
157
  roundStartTime = 0;
72
158
  lastProgressTime = 0;
@@ -83,8 +169,97 @@ export class LoopRuntime extends EventEmitter {
83
169
  getCurrentRound() {
84
170
  return this.currentRound;
85
171
  }
86
- getQualityTrajectory() {
87
- return [...this.qualityTrajectory];
172
+ getSuccessTrajectory() {
173
+ return [...this.successTrajectory];
174
+ }
175
+ // ── Memory Integration (v1.7) ──────────────────────────────────────────
176
+ /** Derive the current progress estimate from the last self evaluation.
177
+ * Returns -1 if no progress data is available. */
178
+ getCurrentProgress() {
179
+ if (!this.lastSelfEval?.execution_evidence)
180
+ return -1;
181
+ const pe = this.lastSelfEval.execution_evidence.progress_estimate;
182
+ return typeof pe === "number" ? pe : -1;
183
+ }
184
+ /** Determine whether memory should be injected this round based on
185
+ * tier-based allowed phases, progress thresholds, and round spacing. */
186
+ shouldInjectMemory() {
187
+ const policy = getPolicy();
188
+ const mi = policy.memory_injection;
189
+ if (!mi.enabled)
190
+ return false;
191
+ if (!this.config.memoryProvider)
192
+ return false;
193
+ if (this.lastInjectionRound > 0 &&
194
+ this.currentRound - this.lastInjectionRound < mi.min_rounds_between_injections) {
195
+ return false;
196
+ }
197
+ // Resolve which phases are allowed for this loop's maxRounds
198
+ const allowedPhases = new Set(resolveAllowedPhases(this.config.maxRounds, mi.round_tiers));
199
+ const maxInjections = allowedPhases.size;
200
+ if (this.injectionCount >= maxInjections)
201
+ return false;
202
+ const progress = this.getCurrentProgress();
203
+ // Phase 1: round 1, always check if allowed
204
+ if (this.currentRound === 1 && this.injectionCount === 0 && allowedPhases.has(1)) {
205
+ return true;
206
+ }
207
+ // Phase 2: progress threshold (only if allowed by tier)
208
+ if (allowedPhases.has(2) &&
209
+ !this.phase2Triggered &&
210
+ progress >= mi.phase_thresholds.phase2.threshold) {
211
+ return true;
212
+ }
213
+ // Phase 3: progress threshold (only if allowed by tier)
214
+ if (allowedPhases.has(3) &&
215
+ !this.phase3Triggered &&
216
+ progress >= mi.phase_thresholds.phase3.threshold) {
217
+ return true;
218
+ }
219
+ return false;
220
+ }
221
+ /** Build the accumulated context for constructing a targeted memory query. */
222
+ buildAccumulatedContext() {
223
+ const recurringIssues = [];
224
+ const failedPatterns = [];
225
+ const keyLessons = [];
226
+ const remainingCriteria = [];
227
+ if (this.lastSelfEval) {
228
+ // Recurring issues: violations that have appeared before
229
+ if (this.lastSelfEval.constraint_violations.length) {
230
+ recurringIssues.push(...this.lastSelfEval.constraint_violations);
231
+ }
232
+ // Remaining criteria
233
+ if (this.lastSelfEval.execution_evidence?.success_criteria_remaining?.length) {
234
+ remainingCriteria.push(...this.lastSelfEval.execution_evidence.success_criteria_remaining);
235
+ }
236
+ // Key lessons from emerged subtasks
237
+ if (this.lastSelfEval.emerged_subtasks?.length) {
238
+ keyLessons.push(...this.lastSelfEval.emerged_subtasks);
239
+ }
240
+ // Wrong assumptions as negative lessons
241
+ if (this.lastSelfEval.wrong_assumptions?.length) {
242
+ keyLessons.push(...this.lastSelfEval.wrong_assumptions.map((a) => `Wrong: ${a}`));
243
+ }
244
+ }
245
+ return { recurringIssues, failedPatterns, keyLessons, remainingCriteria };
246
+ }
247
+ /** Deduplicate external context against previously injected contexts.
248
+ * Returns empty string if the new context is too similar to any prior. */
249
+ dedupAndStoreContext(newContext) {
250
+ if (!newContext.trim())
251
+ return "";
252
+ const policy = getPolicy();
253
+ const threshold = policy.memory_injection.dedup_threshold;
254
+ const newTokens = tokenize(newContext);
255
+ for (const old of this.injectedContexts) {
256
+ const oldTokens = tokenize(old);
257
+ if (jaccard(newTokens, oldTokens) > threshold) {
258
+ return ""; // Too similar — skip
259
+ }
260
+ }
261
+ this.injectedContexts.push(newContext);
262
+ return newContext;
88
263
  }
89
264
  // ── Lifecycle ──────────────────────────────────────────────────────────
90
265
  /** Start the loop. Returns when the loop terminates (task complete,
@@ -100,187 +275,227 @@ export class LoopRuntime extends EventEmitter {
100
275
  let stopReason = "max_rounds";
101
276
  let previousAgentOutput = null;
102
277
  let consecutiveErrors = 0;
103
- for (this.currentRound = 1; this.currentRound <= this.config.maxRounds; this.currentRound++) {
104
- if (this._status !== RuntimeStatus.RUNNING) {
105
- stopReason = this._status === RuntimeStatus.STALLED ? "stalled" : "stopped";
106
- break;
107
- }
108
- // ── Compile ──────────────────────────────────────────────────
109
- const lcr = this.buildCompileRequest(previousAgentOutput);
110
- const compileResult = this.engine.invokeLoopCompile({
111
- task: this.config.task,
112
- mode: Mode.LOOP_COMPILE,
113
- vault_config: makeVaultConfig(),
114
- feedback: null,
115
- skill_name: null,
116
- task_id: null,
117
- loop_id: lcr.loop_id,
118
- round: lcr.round,
119
- goal_id: lcr.goal_id,
120
- domain: lcr.domain ?? "",
121
- plan_source: lcr.plan_source ?? null,
122
- constraints_from_plan: lcr.constraints_from_plan ?? [],
123
- health_check_interval: lcr.health_check_interval,
124
- last_round_result: (lcr.last_round_result ?? undefined),
125
- verification_flags: this.pendingVerificationFlags,
126
- });
127
- if (!compileResult.response?.prompt) {
128
- stopReason = "stalled";
129
- break;
130
- }
131
- const prompt = compileResult.response.prompt;
132
- const analysis = compileResult.response.analysis;
133
- const level = analysis?.rationale?.includes("l0")
134
- ? "l0"
135
- : analysis?.rationale?.includes("l1")
136
- ? "l1"
137
- : "l2";
138
- const technique = analysis?.technique ?? "unknown";
139
- // ── Emit round:start ──────────────────────────────────────────
140
- const startInfo = { round: this.currentRound, level, technique, prompt };
141
- this.emit("round:start", startInfo);
142
- this.config.onRoundStart?.(startInfo);
143
- // ── Execute ───────────────────────────────────────────────────
144
- this.roundStartTime = Date.now();
145
- this.lastProgressTime = this.roundStartTime;
146
- this.timedOut = false;
147
- const ctx = {
148
- round: this.currentRound,
149
- signal: { aborted: false },
150
- reportProgress: (message) => {
151
- this.lastProgressTime = Date.now();
152
- void message; // consumed by heartbeat via lastProgressTime
153
- },
154
- };
155
- this.activeCtx = ctx;
156
- let agentOutput;
157
- try {
158
- agentOutput = await this.config.execute(prompt, ctx);
159
- consecutiveErrors = 0;
160
- }
161
- catch (err) {
162
- consecutiveErrors++;
163
- const dur = Date.now() - this.roundStartTime;
164
- this.activeCtx = null;
165
- // Emit round:complete with error info
166
- const errInfo = {
167
- round: this.currentRound,
168
- quality: 0,
169
- selfEval: makeSelfEvaluation({
170
- success: false,
171
- output_summary: `execute threw: ${String(err).slice(0, 200)}`,
172
- constraint_violations: ["execute() threw an exception"],
173
- should_continue: true,
174
- }),
175
- durationMs: dur,
176
- };
177
- this.emit("round:complete", errInfo);
178
- this.config.onRoundComplete?.(errInfo);
179
- this.qualityTrajectory.push(0);
180
- if (consecutiveErrors >= this.config.maxConsecutiveErrors) {
181
- stopReason = "executor_failure";
278
+ try {
279
+ for (this.currentRound = 1; this.currentRound <= this.config.maxRounds; this.currentRound++) {
280
+ if (this._status !== RuntimeStatus.RUNNING) {
281
+ stopReason = this._status === RuntimeStatus.STALLED ? "stalled" : "stopped";
182
282
  break;
183
283
  }
184
- // Record as a failed round so the next round's compilation
185
- // sees the failure context (forces L1/L2)
186
- previousAgentOutput = null;
187
- continue;
188
- }
189
- const durationMs = Date.now() - this.roundStartTime;
190
- this.activeCtx = null;
191
- // ── Extract self-eval ─────────────────────────────────────────
192
- let selfEval = extractSelfEvaluation(agentOutput);
193
- let extractionSucceeded = selfEval !== null;
194
- if (selfEval === null) {
195
- selfEval = heuristicSelfEvaluation(agentOutput);
196
- extractionSucceeded = false;
197
- }
198
- // ── Auto-feedback ─────────────────────────────────────────────
199
- if (selfEval !== null) {
200
- this.engine.autoFeedback(selfEval, this.config.loopId, this.currentRound, this.config.task);
201
- }
202
- // ── Verification gate (v1.6 unified) ──────────────────────────
203
- // Run cross-round consistency checks between self-eval and lineage.
204
- // Flags are injected into the NEXT round's prompt via the engine.
205
- this.pendingVerificationFlags = [];
206
- let gateContradicted = false;
207
- if (selfEval !== null) {
208
- const vaultEntries = this.engine.getBackend().queryEntries({
209
- prefix: `loop:${this.config.loopId}:r`,
284
+ // ── v1.7: Memory Injection (before compile, only at L2 phases) ──
285
+ this.pendingExternalContext = "";
286
+ if (this.shouldInjectMemory()) {
287
+ const progress = this.getCurrentProgress();
288
+ let phase;
289
+ if (this.currentRound === 1) {
290
+ phase = 1;
291
+ }
292
+ else if (!this.phase2Triggered && progress >= getPolicy().memory_injection.phase_thresholds.phase2.threshold) {
293
+ phase = 2;
294
+ this.phase2Triggered = true;
295
+ }
296
+ else {
297
+ phase = 3;
298
+ this.phase3Triggered = true;
299
+ }
300
+ try {
301
+ const ctx = {
302
+ loopId: this.config.loopId,
303
+ round: this.currentRound,
304
+ task: this.config.task,
305
+ domain: this.config.domain,
306
+ phase,
307
+ progressEstimate: progress,
308
+ accumulatedContext: this.buildAccumulatedContext(),
309
+ };
310
+ const rawContext = await this.config.memoryProvider(ctx);
311
+ this.pendingExternalContext = this.dedupAndStoreContext(rawContext);
312
+ if (this.pendingExternalContext) {
313
+ this.injectionCount++;
314
+ this.lastInjectionRound = this.currentRound;
315
+ logEvent("memory_injected", {
316
+ loopId: this.config.loopId,
317
+ round: this.currentRound,
318
+ phase,
319
+ injectionCount: this.injectionCount,
320
+ contextLength: this.pendingExternalContext.length,
321
+ });
322
+ }
323
+ }
324
+ catch {
325
+ // memoryProvider failed — degrade gracefully, continue without context
326
+ this.pendingExternalContext = "";
327
+ logEvent("memory_provider_error", {
328
+ loopId: this.config.loopId,
329
+ round: this.currentRound,
330
+ });
331
+ }
332
+ }
333
+ // ── Compile ──────────────────────────────────────────────────
334
+ const lcr = this.buildCompileRequest(previousAgentOutput);
335
+ const compileResult = this.engine.invokeLoopCompile({
336
+ task: this.config.task,
337
+ mode: Mode.LOOP_COMPILE,
338
+ vault_config: makeVaultConfig(),
339
+ feedback: null,
340
+ skill_name: null,
341
+ task_id: null,
342
+ loop_id: lcr.loop_id,
343
+ round: lcr.round,
344
+ goal_id: lcr.goal_id,
345
+ domain: lcr.domain ?? "",
346
+ plan_source: lcr.plan_source ?? null,
347
+ constraints_from_plan: lcr.constraints_from_plan ?? [],
348
+ health_check_interval: lcr.health_check_interval,
349
+ last_round_result: (lcr.last_round_result ?? undefined),
350
+ verification_flags: this.pendingVerificationFlags,
351
+ external_context: this.pendingExternalContext || undefined,
210
352
  });
211
- const verifyResult = verifySelfEvaluation(selfEval, this.currentRound, vaultEntries, this.lastSelfEval);
212
- this.pendingVerificationFlags = verifyResult.flags;
213
- if (verifyResult.verdict === "contradicted") {
214
- gateContradicted = true;
215
- logEvent("gate_contradicted", {
216
- loopId: this.config.loopId,
353
+ if (!compileResult.response?.prompt) {
354
+ stopReason = "stalled";
355
+ break;
356
+ }
357
+ const prompt = compileResult.response.prompt;
358
+ const analysis = compileResult.response.analysis;
359
+ const level = analysis?.rationale?.includes("l0")
360
+ ? "l0"
361
+ : analysis?.rationale?.includes("l1")
362
+ ? "l1"
363
+ : "l2";
364
+ const technique = analysis?.technique ?? "unknown";
365
+ // ── Emit round:start ──────────────────────────────────────────
366
+ const startInfo = { round: this.currentRound, level, technique, prompt };
367
+ this.emit("round:start", startInfo);
368
+ this.config.onRoundStart?.(startInfo);
369
+ // ── Execute ───────────────────────────────────────────────────
370
+ this.roundStartTime = Date.now();
371
+ this.lastProgressTime = this.roundStartTime;
372
+ this.timedOut = false;
373
+ const ctx = {
374
+ round: this.currentRound,
375
+ signal: { aborted: false },
376
+ reportProgress: (message) => {
377
+ this.lastProgressTime = Date.now();
378
+ void message; // consumed by heartbeat via lastProgressTime
379
+ },
380
+ };
381
+ this.activeCtx = ctx;
382
+ let agentOutput;
383
+ try {
384
+ agentOutput = await this.config.execute(prompt, ctx);
385
+ consecutiveErrors = 0;
386
+ }
387
+ catch (err) {
388
+ consecutiveErrors++;
389
+ const dur = Date.now() - this.roundStartTime;
390
+ this.activeCtx = null;
391
+ // Emit round:complete with error info
392
+ const errInfo = {
217
393
  round: this.currentRound,
218
- flags: verifyResult.flags.map((f) => f.check),
394
+ roundSuccess: false,
395
+ selfEval: makeSelfEvaluation({
396
+ success: false,
397
+ output_summary: `execute threw: ${String(err).slice(0, 200)}`,
398
+ constraint_violations: ["execute() threw an exception"],
399
+ should_continue: true,
400
+ }),
401
+ durationMs: dur,
402
+ };
403
+ this.emit("round:complete", errInfo);
404
+ this.config.onRoundComplete?.(errInfo);
405
+ this.successTrajectory.push(false);
406
+ if (consecutiveErrors >= this.config.maxConsecutiveErrors) {
407
+ stopReason = "executor_failure";
408
+ break;
409
+ }
410
+ // Record as a failed round so the next round's compilation
411
+ // sees the failure context (forces L1/L2)
412
+ previousAgentOutput = null;
413
+ continue;
414
+ }
415
+ const durationMs = Date.now() - this.roundStartTime;
416
+ this.activeCtx = null;
417
+ // ── Extract self-eval ─────────────────────────────────────────
418
+ let selfEval = extractSelfEvaluation(agentOutput);
419
+ let extractionSucceeded = selfEval !== null;
420
+ if (selfEval === null) {
421
+ selfEval = heuristicSelfEvaluation(agentOutput);
422
+ extractionSucceeded = false;
423
+ }
424
+ // ── Auto-feedback ─────────────────────────────────────────────
425
+ if (selfEval !== null) {
426
+ this.engine.autoFeedback(selfEval, this.config.loopId, this.currentRound, this.config.task);
427
+ }
428
+ // ── Verification gate (v1.6 unified) ──────────────────────────
429
+ // Run cross-round consistency checks between self-eval and lineage.
430
+ // Flags are injected into the NEXT round's prompt via the engine.
431
+ this.pendingVerificationFlags = [];
432
+ let gateContradicted = false;
433
+ if (selfEval !== null) {
434
+ const vaultEntries = this.engine.getBackend().queryEntries({
435
+ prefix: `loop:${this.config.loopId}:r`,
219
436
  });
437
+ const verifyResult = verifySelfEvaluation(selfEval, this.currentRound, vaultEntries, this.lastSelfEval);
438
+ this.pendingVerificationFlags = verifyResult.flags;
439
+ if (verifyResult.verdict === "contradicted") {
440
+ gateContradicted = true;
441
+ logEvent("gate_contradicted", {
442
+ loopId: this.config.loopId,
443
+ round: this.currentRound,
444
+ flags: verifyResult.flags.map((f) => f.check),
445
+ });
446
+ }
447
+ this.lastSelfEval = selfEval;
220
448
  }
221
- this.lastSelfEval = selfEval;
222
- }
223
- // Compute quality score
224
- let quality = 0;
225
- if (selfEval) {
226
- if (selfEval.success && selfEval.constraint_violations.length === 0) {
227
- quality = 5;
449
+ // Compute round success
450
+ const roundSuccess = selfEval?.success ?? false;
451
+ // Contradicted gate verdict: skip success trend (match MCP path behavior)
452
+ if (!gateContradicted) {
453
+ this.successTrajectory.push(roundSuccess);
228
454
  }
229
- else if (selfEval.success) {
230
- quality = 3;
455
+ // ── Emit round:complete ───────────────────────────────────────
456
+ const completeInfo = {
457
+ round: this.currentRound,
458
+ roundSuccess,
459
+ selfEval,
460
+ durationMs,
461
+ };
462
+ this.emit("round:complete", completeInfo);
463
+ this.config.onRoundComplete?.(completeInfo);
464
+ logEvent("round_complete", {
465
+ loopId: this.config.loopId ?? "unknown",
466
+ round: this.currentRound,
467
+ success: roundSuccess,
468
+ durationMs,
469
+ technique: "loop_runtime",
470
+ });
471
+ // ── Check stop conditions ─────────────────────────────────────
472
+ // Extraction check FIRST — heuristic results are low-confidence
473
+ if (!extractionSucceeded) {
474
+ stopReason = "stalled"; // extraction_failure mapped to stalled
475
+ break;
231
476
  }
232
- else if (selfEval.constraint_violations.length > 0) {
233
- quality = 2;
477
+ if (selfEval && !selfEval.should_continue) {
478
+ stopReason = "task_complete";
479
+ break;
234
480
  }
235
- else {
236
- quality = 1;
481
+ if (this.engine.shouldBreak()) {
482
+ stopReason = "circuit_breaker";
483
+ break;
237
484
  }
485
+ if (this.timedOut) {
486
+ // Next round will be forced L2 via the compile request
487
+ this.timedOut = false;
488
+ }
489
+ previousAgentOutput = agentOutput;
238
490
  }
239
- // Contradicted gate verdict: skip quality trend (match MCP path behavior)
240
- if (!gateContradicted) {
241
- this.qualityTrajectory.push(quality);
242
- }
243
- // ── Emit round:complete ───────────────────────────────────────
244
- const completeInfo = {
245
- round: this.currentRound,
246
- quality,
247
- selfEval,
248
- durationMs,
249
- };
250
- this.emit("round:complete", completeInfo);
251
- this.config.onRoundComplete?.(completeInfo);
252
- logEvent("round_complete", {
253
- loopId: this.config.loopId ?? "unknown",
254
- round: this.currentRound,
255
- quality,
256
- durationMs,
257
- technique: "loop_runtime",
258
- });
259
- // ── Check stop conditions ─────────────────────────────────────
260
- // Extraction check FIRST — heuristic results are low-confidence
261
- if (!extractionSucceeded) {
262
- stopReason = "stalled"; // extraction_failure mapped to stalled
263
- break;
264
- }
265
- if (selfEval && !selfEval.should_continue) {
266
- stopReason = "task_complete";
267
- break;
268
- }
269
- if (this.engine.shouldBreak()) {
270
- stopReason = "circuit_breaker";
271
- break;
272
- }
273
- if (this.timedOut) {
274
- // Next round will be forced L2 via the compile request
275
- this.timedOut = false;
276
- }
277
- previousAgentOutput = agentOutput;
278
491
  }
279
- // ── Cleanup ──────────────────────────────────────────────────────
280
- this.stopHeartbeat();
281
- this.unregisterSignalHandlers();
282
- if (this._status === RuntimeStatus.RUNNING) {
283
- this._status = RuntimeStatus.STOPPED;
492
+ finally {
493
+ // ── Cleanup — always runs even if the loop body throws ──────────
494
+ this.stopHeartbeat();
495
+ this.unregisterSignalHandlers();
496
+ if (this._status === RuntimeStatus.RUNNING) {
497
+ this._status = RuntimeStatus.STOPPED;
498
+ }
284
499
  }
285
500
  const result = {
286
501
  success: stopReason === "task_complete",
@@ -288,8 +503,34 @@ export class LoopRuntime extends EventEmitter {
288
503
  roundsCompleted: this.currentRound > this.config.maxRounds
289
504
  ? this.config.maxRounds
290
505
  : this.currentRound,
291
- qualityTrajectory: [...this.qualityTrajectory],
506
+ successTrajectory: [...this.successTrajectory],
292
507
  };
508
+ // ── v1.7: Memory Writeback ───────────────────────────────────────
509
+ const wp = getPolicy().memory_writeback;
510
+ if (wp.enabled && this.config.memoryWriter) {
511
+ const shouldWrite = wp.write_on_outcomes.includes(stopReason) ||
512
+ wp.write_on_outcomes.length === 0;
513
+ if (shouldWrite && result.roundsCompleted > 0) {
514
+ try {
515
+ const payload = buildWritebackPayload(this.config.loopId, this.config.task, stopReason, result);
516
+ if (payload) {
517
+ await this.config.memoryWriter(payload);
518
+ logEvent("memory_writeback", {
519
+ loopId: this.config.loopId,
520
+ stopReason,
521
+ feedbackCount: payload.feedbackEntries.length,
522
+ });
523
+ }
524
+ }
525
+ catch {
526
+ // memoryWriter failed — degrade gracefully
527
+ logEvent("memory_writeback_error", {
528
+ loopId: this.config.loopId,
529
+ stopReason,
530
+ });
531
+ }
532
+ }
533
+ }
293
534
  this.emit("done", result);
294
535
  return result;
295
536
  }
@@ -327,7 +568,6 @@ export class LoopRuntime extends EventEmitter {
327
568
  output_summary: selfEval.output_summary,
328
569
  constraint_violations: selfEval.constraint_violations,
329
570
  manual_fixes_needed: "",
330
- quality_score: 0,
331
571
  // P0–P2: Cognitive evolution
332
572
  discovered_constraints: selfEval.discovered_constraints ?? [],
333
573
  objective_refinement: selfEval.objective_refinement ?? "",
@@ -338,6 +578,9 @@ export class LoopRuntime extends EventEmitter {
338
578
  retracted_constraints: selfEval.retracted_constraints ?? [],
339
579
  revised_success_criteria: selfEval.revised_success_criteria ?? [],
340
580
  wrong_assumptions: selfEval.wrong_assumptions ?? [],
581
+ // v1.10: Checkpoint
582
+ compression_checkpoint: selfEval.compression_checkpoint ?? false,
583
+ checkpoint_label: selfEval.checkpoint_label ?? "",
341
584
  });
342
585
  }
343
586
  }
@@ -453,7 +696,7 @@ export class LoopRuntime extends EventEmitter {
453
696
  * task: 'Audit ERC20 token for security vulnerabilities',
454
697
  * execute: async (prompt) => await myAgent.call(prompt),
455
698
  * });
456
- * // result: { success, stopReason, roundsCompleted, qualityTrajectory }
699
+ * // result: { success, stopReason, roundsCompleted, successTrajectory }
457
700
  */
458
701
  export async function run(rawConfig) {
459
702
  const runtime = new LoopRuntime(rawConfig);