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